Repository: usebruno/bruno Branch: main Commit: 37be72192285 Files: 2445 Total size: 8.9 MB Directory structure: gitextract_14_o08yk/ ├── .coderabbit.yaml ├── .github/ │ ├── CODEOWNERS │ ├── ISSUE_TEMPLATE/ │ │ ├── BugReport.yaml │ │ ├── FeatureRequest.yaml │ │ └── config.yaml │ ├── PULL_REQUEST_TEMPLATE.md │ ├── actions/ │ │ ├── common/ │ │ │ └── setup-node-deps/ │ │ │ └── action.yml │ │ ├── ssl/ │ │ │ ├── linux/ │ │ │ │ ├── run-basic-ssl-cli-tests/ │ │ │ │ │ └── action.yml │ │ │ │ ├── run-custom-ca-certs-cli-tests/ │ │ │ │ │ └── action.yml │ │ │ │ ├── run-ssl-e2e-tests/ │ │ │ │ │ └── action.yml │ │ │ │ ├── setup-ca-certs/ │ │ │ │ │ └── action.yml │ │ │ │ └── setup-feature-specific-deps/ │ │ │ │ └── action.yml │ │ │ ├── macos/ │ │ │ │ ├── run-basic-ssl-cli-tests/ │ │ │ │ │ └── action.yml │ │ │ │ ├── run-custom-ca-certs-cli-tests/ │ │ │ │ │ └── action.yml │ │ │ │ ├── run-ssl-e2e-tests/ │ │ │ │ │ └── action.yml │ │ │ │ ├── setup-ca-certs/ │ │ │ │ │ └── action.yml │ │ │ │ └── setup-feature-specific-deps/ │ │ │ │ └── action.yml │ │ │ └── windows/ │ │ │ ├── run-basic-ssl-cli-tests/ │ │ │ │ └── action.yml │ │ │ ├── run-custom-ca-certs-cli-tests/ │ │ │ │ └── action.yml │ │ │ ├── run-ssl-e2e-tests/ │ │ │ │ └── action.yml │ │ │ └── setup-ca-certs/ │ │ │ └── action.yml │ │ └── tests/ │ │ ├── run-cli-tests/ │ │ │ └── action.yml │ │ ├── run-e2e-tests/ │ │ │ └── action.yml │ │ └── run-unit-tests/ │ │ └── action.yml │ ├── dependabot.yml │ ├── scripts/ │ │ ├── comment-on-flaky-tests.js │ │ └── detect-flaky-tests.js │ └── workflows/ │ ├── flaky-test-detector.yml │ ├── lint-checks.yml │ ├── npm-bru-cli.yml │ ├── ssl-tests.yml │ └── tests.yml ├── .gitignore ├── .husky/ │ └── pre-commit ├── .nvmrc ├── CODING_STANDARDS.md ├── contributing.md ├── docs/ │ ├── contributing/ │ │ ├── contributing_bn.md │ │ ├── contributing_cn.md │ │ ├── contributing_de.md │ │ ├── contributing_es.md │ │ ├── contributing_fa.md │ │ ├── contributing_fr.md │ │ ├── contributing_hi.md │ │ ├── contributing_it.md │ │ ├── contributing_ja.md │ │ ├── contributing_kr.md │ │ ├── contributing_nl.md │ │ ├── contributing_pl.md │ │ ├── contributing_pt_br.md │ │ ├── contributing_ro.md │ │ ├── contributing_ru.md │ │ ├── contributing_sk.md │ │ ├── contributing_tr.md │ │ ├── contributing_ua.md │ │ └── contributing_zhtw.md │ ├── playwright-testing-guide.md │ ├── publishing/ │ │ ├── publishing_bn.md │ │ ├── publishing_cn.md │ │ ├── publishing_de.md │ │ ├── publishing_fa.md │ │ ├── publishing_fr.md │ │ ├── publishing_ja.md │ │ ├── publishing_nl.md │ │ ├── publishing_pl.md │ │ ├── publishing_pt_br.md │ │ ├── publishing_ro.md │ │ ├── publishing_tr.md │ │ └── publishing_zhtw.md │ └── readme/ │ ├── readme_ar.md │ ├── readme_bn.md │ ├── readme_cn.md │ ├── readme_de.md │ ├── readme_es.md │ ├── readme_fa.md │ ├── readme_fr.md │ ├── readme_hi.md │ ├── readme_it.md │ ├── readme_ja.md │ ├── readme_ka.md │ ├── readme_kr.md │ ├── readme_nl.md │ ├── readme_pl.md │ ├── readme_pt_br.md │ ├── readme_ro.md │ ├── readme_ru.md │ ├── readme_tr.md │ ├── readme_ua.md │ └── readme_zhtw.md ├── eslint.config.js ├── license.md ├── package.json ├── packages/ │ ├── bruno-app/ │ │ ├── .babelrc │ │ ├── .gitignore │ │ ├── babel.config.js │ │ ├── jest.config.js │ │ ├── jest.setup.js │ │ ├── jsconfig.json │ │ ├── package.json │ │ ├── postcss.config.js │ │ ├── public/ │ │ │ ├── static/ │ │ │ │ └── diff2Html.js │ │ │ └── theme/ │ │ │ ├── dark.js │ │ │ ├── index.js │ │ │ └── light.js │ │ ├── rsbuild.config.mjs │ │ ├── src/ │ │ │ ├── components/ │ │ │ │ ├── Accordion/ │ │ │ │ │ ├── index.js │ │ │ │ │ └── styledWrapper.js │ │ │ │ ├── ApiSpecPanel/ │ │ │ │ │ ├── FileEditor/ │ │ │ │ │ │ └── CodeEditor/ │ │ │ │ │ │ ├── Plugins/ │ │ │ │ │ │ │ └── Yaml/ │ │ │ │ │ │ │ └── index.js │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ └── index.js │ │ │ │ │ ├── Renderers/ │ │ │ │ │ │ └── Swagger/ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ └── index.js │ │ │ │ │ ├── SpecViewer.js │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ └── index.js │ │ │ │ ├── AppTitleBar/ │ │ │ │ │ ├── AppMenu/ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ └── index.js │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ └── index.js │ │ │ │ ├── BodyModeSelector/ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ └── index.js │ │ │ │ ├── Bruno/ │ │ │ │ │ └── index.js │ │ │ │ ├── BrunoSupport/ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ └── index.js │ │ │ │ ├── BulkEditor/ │ │ │ │ │ └── index.js │ │ │ │ ├── Checkbox/ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ └── index.js │ │ │ │ ├── CodeEditor/ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ ├── index.js │ │ │ │ │ └── index.spec.js │ │ │ │ ├── CodeMirrorSearch/ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ └── index.js │ │ │ │ ├── CollectionSettings/ │ │ │ │ │ ├── Auth/ │ │ │ │ │ │ ├── ApiKeyAuth/ │ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ │ └── index.js │ │ │ │ │ │ ├── AuthMode/ │ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ │ └── index.js │ │ │ │ │ │ ├── AwsV4Auth/ │ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ │ └── index.js │ │ │ │ │ │ ├── BasicAuth/ │ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ │ └── index.js │ │ │ │ │ │ ├── BearerAuth/ │ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ │ └── index.js │ │ │ │ │ │ ├── DigestAuth/ │ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ │ └── index.js │ │ │ │ │ │ ├── NTLMAuth/ │ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ │ └── index.js │ │ │ │ │ │ ├── OAuth2/ │ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ │ └── index.js │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ ├── WsseAuth/ │ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ │ └── index.js │ │ │ │ │ │ └── index.js │ │ │ │ │ ├── ClientCertSettings/ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ └── index.js │ │ │ │ │ ├── Docs/ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ └── index.js │ │ │ │ │ ├── Headers/ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ └── index.js │ │ │ │ │ ├── Overview/ │ │ │ │ │ │ ├── Info/ │ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ │ └── index.js │ │ │ │ │ │ ├── RequestsNotLoaded/ │ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ │ └── index.js │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ └── index.js │ │ │ │ │ ├── Presets/ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ └── index.js │ │ │ │ │ ├── Protobuf/ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ └── index.js │ │ │ │ │ ├── ProxySettings/ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ └── index.js │ │ │ │ │ ├── Script/ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ └── index.js │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ ├── Tests/ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ └── index.js │ │ │ │ │ ├── Vars/ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ ├── VarsTable/ │ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ │ └── index.js │ │ │ │ │ │ └── index.js │ │ │ │ │ └── index.js │ │ │ │ ├── ColorBadge/ │ │ │ │ │ └── index.js │ │ │ │ ├── ColorPicker/ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ └── index.js │ │ │ │ ├── ColorRange/ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ └── index.js │ │ │ │ ├── Cookies/ │ │ │ │ │ ├── ModifyCookieModal/ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ └── index.js │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ └── index.js │ │ │ │ ├── CreateTransientRequest/ │ │ │ │ │ └── index.js │ │ │ │ ├── CreateUntitledRequest/ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ └── index.js │ │ │ │ ├── DeprecationWarning/ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ └── index.js │ │ │ │ ├── Devtools/ │ │ │ │ │ ├── Console/ │ │ │ │ │ │ ├── DebugTab/ │ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ │ └── index.js │ │ │ │ │ │ ├── ErrorDetailsPanel/ │ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ │ └── index.js │ │ │ │ │ │ ├── NetworkTab/ │ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ │ └── index.js │ │ │ │ │ │ ├── RequestDetailsPanel/ │ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ │ └── index.js │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ ├── TerminalTab/ │ │ │ │ │ │ │ ├── SessionList.js │ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ │ └── index.js │ │ │ │ │ │ └── index.js │ │ │ │ │ ├── Performance/ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ └── index.js │ │ │ │ │ └── index.js │ │ │ │ ├── Documentation/ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ └── index.js │ │ │ │ ├── Dropdown/ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ └── index.js │ │ │ │ ├── EditableTable/ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ └── index.js │ │ │ │ ├── EnvironmentVariablesTable/ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ └── index.js │ │ │ │ ├── Environments/ │ │ │ │ │ ├── CollapsibleSection/ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ └── index.js │ │ │ │ │ ├── Common/ │ │ │ │ │ │ ├── ExportEnvironmentModal/ │ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ │ └── index.js │ │ │ │ │ │ └── ImportEnvironmentModal/ │ │ │ │ │ │ └── index.js │ │ │ │ │ ├── ConfirmCloseEnvironment/ │ │ │ │ │ │ └── index.js │ │ │ │ │ ├── DotEnvFileDetails/ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ └── index.js │ │ │ │ │ ├── DotEnvFileEditor/ │ │ │ │ │ │ ├── DotEnvEmptyState.js │ │ │ │ │ │ ├── DotEnvErrorMessage.js │ │ │ │ │ │ ├── DotEnvRawView.js │ │ │ │ │ │ ├── DotEnvTableView.js │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ ├── index.js │ │ │ │ │ │ └── utils.js │ │ │ │ │ ├── EnvironmentSelector/ │ │ │ │ │ │ ├── EnvironmentListContent/ │ │ │ │ │ │ │ └── index.js │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ └── index.js │ │ │ │ │ ├── EnvironmentSettings/ │ │ │ │ │ │ ├── CopyEnvironment/ │ │ │ │ │ │ │ └── index.js │ │ │ │ │ │ ├── CreateEnvironment/ │ │ │ │ │ │ │ └── index.js │ │ │ │ │ │ ├── DeleteDotEnvFile/ │ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ │ └── index.js │ │ │ │ │ │ ├── DeleteEnvironment/ │ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ │ └── index.js │ │ │ │ │ │ ├── EnvironmentList/ │ │ │ │ │ │ │ ├── EnvironmentDetails/ │ │ │ │ │ │ │ │ ├── EnvironmentVariables/ │ │ │ │ │ │ │ │ │ ├── constants.js │ │ │ │ │ │ │ │ │ └── index.js │ │ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ │ │ └── index.js │ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ │ └── index.js │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ └── index.js │ │ │ │ │ └── GlobalEnvironmentSettings/ │ │ │ │ │ └── index.js │ │ │ │ ├── ErrorCapture/ │ │ │ │ │ └── index.js │ │ │ │ ├── Errors/ │ │ │ │ │ └── IpcErrorModal/ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ └── index.js │ │ │ │ ├── FilePickerEditor/ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ └── index.js │ │ │ │ ├── FolderSettings/ │ │ │ │ │ ├── Auth/ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ └── index.js │ │ │ │ │ ├── AuthMode/ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ └── index.js │ │ │ │ │ ├── Documentation/ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ └── index.js │ │ │ │ │ ├── Headers/ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ └── index.js │ │ │ │ │ ├── Script/ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ └── index.js │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ ├── Tests/ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ └── index.js │ │ │ │ │ ├── Vars/ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ ├── VarsTable/ │ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ │ └── index.js │ │ │ │ │ │ └── index.js │ │ │ │ │ └── index.js │ │ │ │ ├── Git/ │ │ │ │ │ ├── GitNotFoundModal/ │ │ │ │ │ │ └── index.js │ │ │ │ │ └── VisualDiffViewer/ │ │ │ │ │ ├── CollapsibleDiffRow.js │ │ │ │ │ ├── VisualDiffAuth.js │ │ │ │ │ ├── VisualDiffBody.js │ │ │ │ │ ├── VisualDiffContent/ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ └── index.js │ │ │ │ │ ├── VisualDiffHeaders.js │ │ │ │ │ ├── VisualDiffParams.js │ │ │ │ │ ├── VisualDiffUrlBar.js │ │ │ │ │ └── utils/ │ │ │ │ │ ├── bruUtils.js │ │ │ │ │ ├── bruUtils.spec.js │ │ │ │ │ ├── diffUtils.js │ │ │ │ │ └── diffUtils.spec.js │ │ │ │ ├── GlobalSearchModal/ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ ├── constants/ │ │ │ │ │ │ └── index.js │ │ │ │ │ ├── index.js │ │ │ │ │ └── utils/ │ │ │ │ │ └── searchUtils.js │ │ │ │ ├── Help/ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ └── index.js │ │ │ │ ├── Icons/ │ │ │ │ │ ├── CloseAll/ │ │ │ │ │ │ └── index.js │ │ │ │ │ ├── Dot/ │ │ │ │ │ │ └── index.js │ │ │ │ │ ├── ExampleIcon/ │ │ │ │ │ │ └── index.js │ │ │ │ │ ├── Grpc/ │ │ │ │ │ │ └── index.js │ │ │ │ │ ├── IconAlertTriangleFilled/ │ │ │ │ │ │ └── index.js │ │ │ │ │ ├── IconBottombarToggle/ │ │ │ │ │ │ └── index.js │ │ │ │ │ ├── IconCaretDown/ │ │ │ │ │ │ └── index.js │ │ │ │ │ ├── IconCheckMark/ │ │ │ │ │ │ └── index.js │ │ │ │ │ ├── IconEdit/ │ │ │ │ │ │ └── index.js │ │ │ │ │ ├── IconSidebarToggle/ │ │ │ │ │ │ └── index.js │ │ │ │ │ ├── InfoCircle/ │ │ │ │ │ │ └── index.js │ │ │ │ │ ├── OpenAPILogo/ │ │ │ │ │ │ └── index.js │ │ │ │ │ ├── OpenAPISync/ │ │ │ │ │ │ └── index.js │ │ │ │ │ ├── OpenCollectionIcon/ │ │ │ │ │ │ └── index.js │ │ │ │ │ ├── QuestionCircle/ │ │ │ │ │ │ └── index.js │ │ │ │ │ └── Send/ │ │ │ │ │ └── index.js │ │ │ │ ├── InfoTip/ │ │ │ │ │ └── index.js │ │ │ │ ├── InheritableSettingsInput/ │ │ │ │ │ └── index.js │ │ │ │ ├── ManageWorkspace/ │ │ │ │ │ ├── DeleteWorkspace/ │ │ │ │ │ │ └── index.js │ │ │ │ │ ├── RenameWorkspace/ │ │ │ │ │ │ └── index.js │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ └── index.js │ │ │ │ ├── MarkDown/ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ └── index.jsx │ │ │ │ ├── Modal/ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ └── index.js │ │ │ │ ├── MultiLineEditor/ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ └── index.js │ │ │ │ ├── Notifications/ │ │ │ │ │ ├── StyleWrapper.js │ │ │ │ │ └── index.js │ │ │ │ ├── OpenAPISpecTab/ │ │ │ │ │ └── index.js │ │ │ │ ├── OpenAPISyncTab/ │ │ │ │ │ ├── CollectionStatusSection/ │ │ │ │ │ │ └── index.js │ │ │ │ │ ├── ConfirmSyncModal/ │ │ │ │ │ │ └── index.js │ │ │ │ │ ├── ConnectSpecForm/ │ │ │ │ │ │ └── index.js │ │ │ │ │ ├── ConnectionSettingsModal/ │ │ │ │ │ │ └── index.js │ │ │ │ │ ├── DisconnectSyncModal/ │ │ │ │ │ │ └── index.js │ │ │ │ │ ├── EndpointChangeSection/ │ │ │ │ │ │ ├── EndpointItem.js │ │ │ │ │ │ ├── EndpointVisualDiff.js │ │ │ │ │ │ ├── ExpandableEndpointRow.js │ │ │ │ │ │ └── index.js │ │ │ │ │ ├── OpenAPISyncHeader/ │ │ │ │ │ │ └── index.js │ │ │ │ │ ├── OverviewSection/ │ │ │ │ │ │ └── index.js │ │ │ │ │ ├── SpecDiffModal/ │ │ │ │ │ │ └── index.js │ │ │ │ │ ├── SpecStatusSection/ │ │ │ │ │ │ └── index.js │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ ├── SyncReviewPage/ │ │ │ │ │ │ └── index.js │ │ │ │ │ ├── hooks/ │ │ │ │ │ │ ├── useEndpointActions.js │ │ │ │ │ │ ├── useOpenAPISync.js │ │ │ │ │ │ └── useSyncFlow.js │ │ │ │ │ ├── index.js │ │ │ │ │ └── utils.js │ │ │ │ ├── PathDisplay/ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ └── index.js │ │ │ │ ├── Portal/ │ │ │ │ │ └── index.js │ │ │ │ ├── Preferences/ │ │ │ │ │ ├── Beta/ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ └── index.js │ │ │ │ │ ├── Cache/ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ └── index.js │ │ │ │ │ ├── Display/ │ │ │ │ │ │ ├── Font/ │ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ │ └── index.js │ │ │ │ │ │ ├── Theme/ │ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ │ └── index.js │ │ │ │ │ │ ├── Zoom/ │ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ │ └── index.js │ │ │ │ │ │ └── index.js │ │ │ │ │ ├── General/ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ └── index.js │ │ │ │ │ ├── Keybindings/ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ └── index.js │ │ │ │ │ ├── ProxySettings/ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ ├── SystemProxy/ │ │ │ │ │ │ │ └── index.js │ │ │ │ │ │ └── index.js │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ ├── Support/ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ └── index.js │ │ │ │ │ ├── Themes/ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ └── index.js │ │ │ │ │ └── index.js │ │ │ │ ├── RadioButton/ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ └── index.js │ │ │ │ ├── ReorderTable/ │ │ │ │ │ └── index.js │ │ │ │ ├── RequestPane/ │ │ │ │ │ ├── Assertions/ │ │ │ │ │ │ ├── AssertionOperator/ │ │ │ │ │ │ │ └── index.js │ │ │ │ │ │ ├── AssertionRow/ │ │ │ │ │ │ │ └── index.js │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ └── index.js │ │ │ │ │ ├── Auth/ │ │ │ │ │ │ ├── ApiKeyAuth/ │ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ │ └── index.js │ │ │ │ │ │ ├── AuthMode/ │ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ │ └── index.js │ │ │ │ │ │ ├── AwsV4Auth/ │ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ │ └── index.js │ │ │ │ │ │ ├── BasicAuth/ │ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ │ └── index.js │ │ │ │ │ │ ├── BearerAuth/ │ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ │ └── index.js │ │ │ │ │ │ ├── DigestAuth/ │ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ │ └── index.js │ │ │ │ │ │ ├── NTLMAuth/ │ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ │ └── index.js │ │ │ │ │ │ ├── OAuth2/ │ │ │ │ │ │ │ ├── AdditionalParams/ │ │ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ │ │ └── index.js │ │ │ │ │ │ │ ├── AuthorizationCode/ │ │ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ │ │ ├── index.js │ │ │ │ │ │ │ │ └── inputsConfig.js │ │ │ │ │ │ │ ├── ClientCredentials/ │ │ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ │ │ ├── index.js │ │ │ │ │ │ │ │ └── inputsConfig.js │ │ │ │ │ │ │ ├── GrantTypeSelector/ │ │ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ │ │ └── index.js │ │ │ │ │ │ │ ├── Implicit/ │ │ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ │ │ ├── index.js │ │ │ │ │ │ │ │ └── inputsConfig.js │ │ │ │ │ │ │ ├── Oauth2ActionButtons/ │ │ │ │ │ │ │ │ └── index.js │ │ │ │ │ │ │ ├── Oauth2TokenViewer/ │ │ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ │ │ └── index.js │ │ │ │ │ │ │ ├── PasswordCredentials/ │ │ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ │ │ ├── index.js │ │ │ │ │ │ │ │ └── inputsConfig.js │ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ │ └── index.js │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ ├── WsseAuth/ │ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ │ └── index.js │ │ │ │ │ │ └── index.js │ │ │ │ │ ├── FileBody/ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ └── index.js │ │ │ │ │ ├── FormUrlEncodedParams/ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ └── index.js │ │ │ │ │ ├── GraphQLRequestPane/ │ │ │ │ │ │ └── index.js │ │ │ │ │ ├── GraphQLSchemaActions/ │ │ │ │ │ │ ├── index.js │ │ │ │ │ │ └── useGraphqlSchema.js │ │ │ │ │ ├── GraphQLVariables/ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ └── index.js │ │ │ │ │ ├── GrpcBody/ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ └── index.js │ │ │ │ │ ├── GrpcQueryUrl/ │ │ │ │ │ │ ├── GrpcurlModal/ │ │ │ │ │ │ │ └── index.js │ │ │ │ │ │ ├── MethodDropdown/ │ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ │ └── index.js │ │ │ │ │ │ ├── ProtoFileDropdown/ │ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ │ └── index.js │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ ├── Tabs/ │ │ │ │ │ │ │ ├── ImportPathsTab/ │ │ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ │ │ └── index.js │ │ │ │ │ │ │ ├── ProtoFilesTab/ │ │ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ │ │ └── index.js │ │ │ │ │ │ │ ├── TabNavigation/ │ │ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ │ │ └── index.js │ │ │ │ │ │ │ └── index.js │ │ │ │ │ │ └── index.js │ │ │ │ │ ├── GrpcRequestPane/ │ │ │ │ │ │ ├── GrpcAuth/ │ │ │ │ │ │ │ ├── GrpcAuthMode/ │ │ │ │ │ │ │ │ └── index.js │ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ │ └── index.js │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ └── index.js │ │ │ │ │ ├── HttpRequestPane/ │ │ │ │ │ │ └── index.js │ │ │ │ │ ├── MultipartFormParams/ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ └── index.js │ │ │ │ │ ├── PromptVariables/ │ │ │ │ │ │ └── PromptVariablesModal/ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ └── index.js │ │ │ │ │ ├── QueryEditor/ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ ├── index.js │ │ │ │ │ │ └── onHasCompletion.js │ │ │ │ │ ├── QueryParams/ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ └── index.js │ │ │ │ │ ├── QueryUrl/ │ │ │ │ │ │ ├── HttpMethodSelector/ │ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ │ ├── index.js │ │ │ │ │ │ │ └── index.spec.js │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ └── index.js │ │ │ │ │ ├── RequestBody/ │ │ │ │ │ │ ├── RequestBodyMode/ │ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ │ └── index.js │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ └── index.js │ │ │ │ │ ├── RequestHeaders/ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ └── index.js │ │ │ │ │ ├── Script/ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ └── index.js │ │ │ │ │ ├── Settings/ │ │ │ │ │ │ ├── Tags/ │ │ │ │ │ │ │ └── index.js │ │ │ │ │ │ ├── ToggleSelector/ │ │ │ │ │ │ │ └── index.js │ │ │ │ │ │ └── index.js │ │ │ │ │ ├── Tests/ │ │ │ │ │ │ └── index.js │ │ │ │ │ ├── Vars/ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ ├── VarsTable/ │ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ │ └── index.js │ │ │ │ │ │ └── index.js │ │ │ │ │ ├── WSRequestPane/ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ ├── WSAuth/ │ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ │ ├── WSAuthMode/ │ │ │ │ │ │ │ │ └── index.js │ │ │ │ │ │ │ └── index.js │ │ │ │ │ │ └── index.js │ │ │ │ │ ├── WSSettingsPane/ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ └── index.js │ │ │ │ │ ├── WsBody/ │ │ │ │ │ │ ├── BodyMode/ │ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ │ └── index.js │ │ │ │ │ │ ├── SingleWSMessage/ │ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ │ └── index.js │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ └── index.js │ │ │ │ │ └── WsQueryUrl/ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ └── index.js │ │ │ │ ├── RequestTabPanel/ │ │ │ │ │ ├── ExampleNotFound/ │ │ │ │ │ │ └── index.js │ │ │ │ │ ├── FolderNotFound/ │ │ │ │ │ │ └── index.js │ │ │ │ │ ├── RequestIsLoading/ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ └── index.js │ │ │ │ │ ├── RequestNotFound/ │ │ │ │ │ │ └── index.js │ │ │ │ │ ├── RequestNotLoaded/ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ └── index.js │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ └── index.js │ │ │ │ ├── RequestTabs/ │ │ │ │ │ ├── CollectionHeader/ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ └── index.js │ │ │ │ │ ├── DraggableTab.js │ │ │ │ │ ├── ExampleTab/ │ │ │ │ │ │ └── index.js │ │ │ │ │ ├── RequestTab/ │ │ │ │ │ │ ├── CloseTabIcon.js │ │ │ │ │ │ ├── ConfirmCollectionClose/ │ │ │ │ │ │ │ └── index.js │ │ │ │ │ │ ├── ConfirmFolderClose/ │ │ │ │ │ │ │ └── index.js │ │ │ │ │ │ ├── ConfirmRequestClose/ │ │ │ │ │ │ │ └── index.js │ │ │ │ │ │ ├── DraftTabIcon.js │ │ │ │ │ │ ├── GradientCloseButton/ │ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ │ └── index.js │ │ │ │ │ │ ├── RequestTabNotFound.js │ │ │ │ │ │ ├── SpecialTab.js │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ └── index.js │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ └── index.js │ │ │ │ ├── ResponseExample/ │ │ │ │ │ ├── CreateExampleModal/ │ │ │ │ │ │ └── index.js │ │ │ │ │ ├── ResponseExampleRequestPane/ │ │ │ │ │ │ ├── ResponseExampleBody/ │ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ │ └── index.js │ │ │ │ │ │ ├── ResponseExampleBodyMode/ │ │ │ │ │ │ │ └── index.js │ │ │ │ │ │ ├── ResponseExampleBodyRenderer/ │ │ │ │ │ │ │ └── index.js │ │ │ │ │ │ ├── ResponseExampleDescription/ │ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ │ └── index.js │ │ │ │ │ │ ├── ResponseExampleFileBody/ │ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ │ └── index.js │ │ │ │ │ │ ├── ResponseExampleFormUrlEncodedParams/ │ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ │ └── index.js │ │ │ │ │ │ ├── ResponseExampleHeaders/ │ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ │ └── index.js │ │ │ │ │ │ ├── ResponseExampleMultipartFormParams/ │ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ │ └── index.js │ │ │ │ │ │ ├── ResponseExampleParams/ │ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ │ └── index.js │ │ │ │ │ │ ├── ResponseExampleUrlBar/ │ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ │ └── index.js │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ └── index.js │ │ │ │ │ ├── ResponseExampleResponsePane/ │ │ │ │ │ │ ├── ResponseExampleResponseContent/ │ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ │ └── index.js │ │ │ │ │ │ ├── ResponseExampleResponseHeaders/ │ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ │ └── index.js │ │ │ │ │ │ ├── ResponseExampleStatusInput/ │ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ │ └── index.js │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ └── index.js │ │ │ │ │ ├── ResponseExampleTopBar/ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ └── index.js │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ └── index.js │ │ │ │ ├── ResponsePane/ │ │ │ │ │ ├── ClearTimeline/ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ └── index.js │ │ │ │ │ ├── GrpcResponsePane/ │ │ │ │ │ │ ├── GrpcError/ │ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ │ └── index.js │ │ │ │ │ │ ├── GrpcQueryResult/ │ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ │ └── index.js │ │ │ │ │ │ ├── GrpcResponseHeaders/ │ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ │ └── index.js │ │ │ │ │ │ ├── GrpcStatusCode/ │ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ │ ├── get-grpc-status-code-phrase.js │ │ │ │ │ │ │ └── index.js │ │ │ │ │ │ ├── ResponseTrailers/ │ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ │ └── index.js │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ └── index.js │ │ │ │ │ ├── LargeResponseWarning/ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ └── index.js │ │ │ │ │ ├── NetworkError/ │ │ │ │ │ │ └── index.js │ │ │ │ │ ├── Overlay/ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ └── index.js │ │ │ │ │ ├── Placeholder/ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ └── index.js │ │ │ │ │ ├── QueryResponse/ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ └── index.js │ │ │ │ │ ├── QueryResult/ │ │ │ │ │ │ ├── QueryResultFilter/ │ │ │ │ │ │ │ └── index.js │ │ │ │ │ │ ├── QueryResultPreview/ │ │ │ │ │ │ │ ├── HtmlPreview.js │ │ │ │ │ │ │ ├── JsonPreview.js │ │ │ │ │ │ │ ├── TextPreview.js │ │ │ │ │ │ │ ├── VideoPreview.js │ │ │ │ │ │ │ ├── XmlPreview/ │ │ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ │ │ └── index.js │ │ │ │ │ │ │ └── index.js │ │ │ │ │ │ ├── QueryResultTypeSelector/ │ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ │ └── index.jsx │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ └── index.js │ │ │ │ │ ├── ResponseActions/ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ └── index.js │ │ │ │ │ ├── ResponseBookmark/ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ └── index.js │ │ │ │ │ ├── ResponseClear/ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ └── index.js │ │ │ │ │ ├── ResponseCopy/ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ └── index.js │ │ │ │ │ ├── ResponseDownload/ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ └── index.js │ │ │ │ │ ├── ResponseHeaders/ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ └── index.js │ │ │ │ │ ├── ResponseLayoutToggle/ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ ├── index.js │ │ │ │ │ │ └── index.spec.js │ │ │ │ │ ├── ResponsePaneActions/ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ └── index.js │ │ │ │ │ ├── ResponseSize/ │ │ │ │ │ │ ├── ResponseSize.spec.js │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ └── index.js │ │ │ │ │ ├── ResponseStopWatch/ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ └── index.js │ │ │ │ │ ├── ResponseTime/ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ └── index.js │ │ │ │ │ ├── RunnerTimeline/ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ └── index.js │ │ │ │ │ ├── ScriptError/ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ └── index.js │ │ │ │ │ ├── ScriptErrorIcon/ │ │ │ │ │ │ └── index.js │ │ │ │ │ ├── SkippedRequest/ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ └── index.js │ │ │ │ │ ├── StatusCode/ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ ├── get-status-code-phrase.js │ │ │ │ │ │ └── index.js │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ ├── TestResults/ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ └── index.js │ │ │ │ │ ├── TestResultsLabel/ │ │ │ │ │ │ └── index.js │ │ │ │ │ ├── Timeline/ │ │ │ │ │ │ ├── GrpcTimelineItem/ │ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ │ └── index.js │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ ├── TimelineItem/ │ │ │ │ │ │ │ ├── Common/ │ │ │ │ │ │ │ │ ├── Body/ │ │ │ │ │ │ │ │ │ └── index.js │ │ │ │ │ │ │ │ ├── Headers/ │ │ │ │ │ │ │ │ │ └── index.js │ │ │ │ │ │ │ │ ├── Method/ │ │ │ │ │ │ │ │ │ └── index.js │ │ │ │ │ │ │ │ ├── Status/ │ │ │ │ │ │ │ │ │ └── index.js │ │ │ │ │ │ │ │ └── Time/ │ │ │ │ │ │ │ │ └── index.js │ │ │ │ │ │ │ ├── Network/ │ │ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ │ │ └── index.js │ │ │ │ │ │ │ ├── Request/ │ │ │ │ │ │ │ │ └── index.js │ │ │ │ │ │ │ ├── Response/ │ │ │ │ │ │ │ │ └── index.js │ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ │ └── index.js │ │ │ │ │ │ └── index.js │ │ │ │ │ ├── WsResponsePane/ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ ├── WSMessagesList/ │ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ │ └── index.js │ │ │ │ │ │ ├── WSResponseHeaders/ │ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ │ └── index.js │ │ │ │ │ │ ├── WSResponseSortOrder/ │ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ │ └── index.js │ │ │ │ │ │ ├── WSStatusCode/ │ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ │ ├── get-ws-status-code-phrase.js │ │ │ │ │ │ │ └── index.js │ │ │ │ │ │ └── index.js │ │ │ │ │ └── index.js │ │ │ │ ├── RunnerResults/ │ │ │ │ │ ├── ResponsePane/ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ └── index.js │ │ │ │ │ ├── RunConfigurationPanel/ │ │ │ │ │ │ ├── StyledWrapper.jsx │ │ │ │ │ │ └── index.jsx │ │ │ │ │ ├── RunnerTags/ │ │ │ │ │ │ └── index.jsx │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ └── index.jsx │ │ │ │ ├── SaveTransientRequest/ │ │ │ │ │ ├── CollectionListItem/ │ │ │ │ │ │ └── index.js │ │ │ │ │ ├── Container.js │ │ │ │ │ ├── FolderBreadcrumbs/ │ │ │ │ │ │ └── index.js │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ └── index.js │ │ │ │ ├── SearchInput/ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ └── index.js │ │ │ │ ├── SecuritySettings/ │ │ │ │ │ └── JsSandboxMode/ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ └── index.js │ │ │ │ ├── SensitiveFieldWarning/ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ └── index.js │ │ │ │ ├── SettingsInput/ │ │ │ │ │ └── index.js │ │ │ │ ├── ShareCollection/ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ └── index.js │ │ │ │ ├── Sidebar/ │ │ │ │ │ ├── ApiSpecs/ │ │ │ │ │ │ ├── ApiSpecItem/ │ │ │ │ │ │ │ └── index.js │ │ │ │ │ │ ├── CloseApiSpec/ │ │ │ │ │ │ │ └── index.js │ │ │ │ │ │ ├── CreateApiSpec/ │ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ │ └── index.js │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ └── index.js │ │ │ │ │ ├── BulkImportCollectionLocation/ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ ├── index.js │ │ │ │ │ │ └── index.spec.js │ │ │ │ │ ├── CloneGitRespository/ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ └── index.js │ │ │ │ │ ├── CloseWorkspace/ │ │ │ │ │ │ └── index.js │ │ │ │ │ ├── Collections/ │ │ │ │ │ │ ├── Collection/ │ │ │ │ │ │ │ ├── CloneCollection/ │ │ │ │ │ │ │ │ └── index.js │ │ │ │ │ │ │ ├── CollectionItem/ │ │ │ │ │ │ │ │ ├── CloneCollectionItem/ │ │ │ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ │ │ │ └── index.js │ │ │ │ │ │ │ │ ├── CollectionItemDragPreview/ │ │ │ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ │ │ │ └── index.js │ │ │ │ │ │ │ │ ├── CollectionItemIcon/ │ │ │ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ │ │ │ └── index.js │ │ │ │ │ │ │ │ ├── CollectionItemInfo/ │ │ │ │ │ │ │ │ │ └── index.js │ │ │ │ │ │ │ │ ├── DeleteCollectionItem/ │ │ │ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ │ │ │ └── index.js │ │ │ │ │ │ │ │ ├── ExampleItem/ │ │ │ │ │ │ │ │ │ ├── DeleteResponseExampleModal/ │ │ │ │ │ │ │ │ │ │ └── index.js │ │ │ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ │ │ │ └── index.js │ │ │ │ │ │ │ │ ├── GenerateCodeItem/ │ │ │ │ │ │ │ │ │ ├── CodeView/ │ │ │ │ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ │ │ │ │ └── index.js │ │ │ │ │ │ │ │ │ ├── CodeViewToolbar/ │ │ │ │ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ │ │ │ │ └── index.js │ │ │ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ │ │ │ ├── index.js │ │ │ │ │ │ │ │ │ └── utils/ │ │ │ │ │ │ │ │ │ ├── interpolation.js │ │ │ │ │ │ │ │ │ ├── interpolation.spec.js │ │ │ │ │ │ │ │ │ ├── snippet-generator.js │ │ │ │ │ │ │ │ │ └── snippet-generator.spec.js │ │ │ │ │ │ │ │ ├── RenameCollectionItem/ │ │ │ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ │ │ │ └── index.js │ │ │ │ │ │ │ │ ├── RequestMethod/ │ │ │ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ │ │ │ └── index.js │ │ │ │ │ │ │ │ ├── RunCollectionItem/ │ │ │ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ │ │ │ └── index.js │ │ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ │ │ └── index.js │ │ │ │ │ │ │ ├── DeleteCollection/ │ │ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ │ │ └── index.js │ │ │ │ │ │ │ ├── GenerateDocumentation/ │ │ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ │ │ └── index.js │ │ │ │ │ │ │ ├── RemoveCollection/ │ │ │ │ │ │ │ │ ├── ConfirmCollectionCloseDrafts.js │ │ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ │ │ └── index.js │ │ │ │ │ │ │ ├── RenameCollection/ │ │ │ │ │ │ │ │ └── index.js │ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ │ └── index.js │ │ │ │ │ │ ├── CollectionSearch/ │ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ │ └── index.js │ │ │ │ │ │ ├── CreateOrOpenCollection/ │ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ │ └── index.js │ │ │ │ │ │ ├── InlineCollectionCreator/ │ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ │ └── index.js │ │ │ │ │ │ ├── RemoveCollectionsModal/ │ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ │ └── index.js │ │ │ │ │ │ ├── SelectCollection/ │ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ │ └── index.js │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ └── index.js │ │ │ │ │ ├── CreateCollection/ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ └── index.js │ │ │ │ │ ├── GoldenEdition/ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ └── index.js │ │ │ │ │ ├── ImportCollection/ │ │ │ │ │ │ ├── FileTab.js │ │ │ │ │ │ ├── FullscreenLoader/ │ │ │ │ │ │ │ └── index.js │ │ │ │ │ │ ├── GitHubTab.js │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ ├── UrlTab.js │ │ │ │ │ │ └── index.js │ │ │ │ │ ├── ImportCollectionLocation/ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ └── index.js │ │ │ │ │ ├── NewFolder/ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ └── index.js │ │ │ │ │ ├── NewRequest/ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ └── index.js │ │ │ │ │ ├── Sections/ │ │ │ │ │ │ ├── ApiSpecsSection/ │ │ │ │ │ │ │ └── index.js │ │ │ │ │ │ └── CollectionsSection/ │ │ │ │ │ │ └── index.js │ │ │ │ │ ├── SidebarAccordionContext.js │ │ │ │ │ ├── SidebarContent.js │ │ │ │ │ ├── SidebarSection/ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ └── index.js │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ └── index.js │ │ │ │ ├── SingleLineEditor/ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ └── index.js │ │ │ │ ├── Spinner/ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ └── index.js │ │ │ │ ├── StatusBar/ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ ├── ThemeDropdown/ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ └── index.js │ │ │ │ │ └── index.js │ │ │ │ ├── StatusDot/ │ │ │ │ │ └── index.js │ │ │ │ ├── StopWatch/ │ │ │ │ │ └── index.js │ │ │ │ ├── Tab/ │ │ │ │ │ └── index.js │ │ │ │ ├── Table/ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ └── index.js │ │ │ │ ├── Tabs/ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ └── index.js │ │ │ │ ├── TagList/ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ └── index.js │ │ │ │ ├── ToggleSwitch/ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ └── index.js │ │ │ │ ├── ToolHint/ │ │ │ │ │ └── index.js │ │ │ │ ├── TruncatedText/ │ │ │ │ │ └── index.js │ │ │ │ ├── VariablesEditor/ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ └── index.js │ │ │ │ ├── WelcomeModal/ │ │ │ │ │ ├── GetStartedStep/ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ └── index.js │ │ │ │ │ ├── StorageStep/ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ └── index.js │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ ├── ThemeStep/ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ └── index.js │ │ │ │ │ ├── WelcomeStep/ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ └── index.js │ │ │ │ │ └── index.js │ │ │ │ ├── WorkspaceHome/ │ │ │ │ │ ├── WorkspaceDocs/ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ └── index.js │ │ │ │ │ ├── WorkspaceEnvironments/ │ │ │ │ │ │ ├── CopyEnvironment/ │ │ │ │ │ │ │ └── index.js │ │ │ │ │ │ ├── CreateEnvironment/ │ │ │ │ │ │ │ └── index.js │ │ │ │ │ │ ├── DeleteEnvironment/ │ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ │ └── index.js │ │ │ │ │ │ ├── EnvironmentList/ │ │ │ │ │ │ │ ├── ConfirmSwitchEnv.js │ │ │ │ │ │ │ ├── EnvironmentDetails/ │ │ │ │ │ │ │ │ ├── EnvironmentVariables/ │ │ │ │ │ │ │ │ │ └── index.js │ │ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ │ │ └── index.js │ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ │ └── index.js │ │ │ │ │ │ ├── ImportEnvironment/ │ │ │ │ │ │ │ └── index.js │ │ │ │ │ │ ├── RenameEnvironment/ │ │ │ │ │ │ │ └── index.js │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ └── index.js │ │ │ │ │ └── WorkspaceOverview/ │ │ │ │ │ ├── CollectionsList/ │ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ │ └── index.js │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ └── index.js │ │ │ │ └── WorkspaceSidebar/ │ │ │ │ ├── CreateWorkspace/ │ │ │ │ │ └── index.js │ │ │ │ └── ImportWorkspace/ │ │ │ │ └── index.js │ │ │ ├── globalStyles.js │ │ │ ├── hooks/ │ │ │ │ ├── useCollectionFolderTree/ │ │ │ │ │ └── index.js │ │ │ │ ├── useDebounce/ │ │ │ │ │ └── index.js │ │ │ │ ├── useDeferredLoading/ │ │ │ │ │ ├── index.js │ │ │ │ │ └── index.spec.js │ │ │ │ ├── useDetectSensitiveField/ │ │ │ │ │ └── index.js │ │ │ │ ├── useFocusTrap/ │ │ │ │ │ └── index.js │ │ │ │ ├── useLocalStorage/ │ │ │ │ │ └── index.js │ │ │ │ ├── useOnClickOutside/ │ │ │ │ │ └── index.js │ │ │ │ ├── usePrevious/ │ │ │ │ │ └── index.js │ │ │ │ ├── useProtoFileManagement/ │ │ │ │ │ └── index.js │ │ │ │ ├── useReflectionManagement/ │ │ │ │ │ └── index.js │ │ │ │ └── useTabPaneBoundaries/ │ │ │ │ └── index.js │ │ │ ├── i18n/ │ │ │ │ ├── index.js │ │ │ │ └── translation/ │ │ │ │ └── en.json │ │ │ ├── index.js │ │ │ ├── pages/ │ │ │ │ ├── Bruno/ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ └── index.js │ │ │ │ ├── ErrorBoundary/ │ │ │ │ │ └── index.js │ │ │ │ ├── Main.js │ │ │ │ └── index.js │ │ │ ├── providers/ │ │ │ │ ├── App/ │ │ │ │ │ ├── ConfirmAppClose/ │ │ │ │ │ │ ├── SaveRequestsModal.js │ │ │ │ │ │ └── index.js │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ ├── index.js │ │ │ │ │ ├── useIpcEvents.js │ │ │ │ │ ├── useOpenAPISyncPolling.js │ │ │ │ │ └── useTelemetry.js │ │ │ │ ├── Hotkeys/ │ │ │ │ │ ├── index.js │ │ │ │ │ └── keyMappings.js │ │ │ │ ├── PromptVariables/ │ │ │ │ │ └── index.js │ │ │ │ ├── ReduxStore/ │ │ │ │ │ ├── index.js │ │ │ │ │ ├── middlewares/ │ │ │ │ │ │ ├── autosave/ │ │ │ │ │ │ │ └── middleware.js │ │ │ │ │ │ ├── debug/ │ │ │ │ │ │ │ └── middleware.js │ │ │ │ │ │ ├── draft/ │ │ │ │ │ │ │ ├── middleware.js │ │ │ │ │ │ │ └── utils.js │ │ │ │ │ │ └── tasks/ │ │ │ │ │ │ ├── middleware.js │ │ │ │ │ │ └── utils.js │ │ │ │ │ └── slices/ │ │ │ │ │ ├── apiSpec.js │ │ │ │ │ ├── app.js │ │ │ │ │ ├── collections/ │ │ │ │ │ │ ├── actions.js │ │ │ │ │ │ ├── exampleReducers.js │ │ │ │ │ │ └── index.js │ │ │ │ │ ├── global-environments.js │ │ │ │ │ ├── logs.js │ │ │ │ │ ├── notifications.js │ │ │ │ │ ├── notifications.spec.js │ │ │ │ │ ├── openapi-sync.js │ │ │ │ │ ├── performance.js │ │ │ │ │ ├── tabs.js │ │ │ │ │ └── workspaces/ │ │ │ │ │ ├── actions.js │ │ │ │ │ ├── getTabToFocusForCurrentWorkspace.js │ │ │ │ │ ├── getTabToFocusForCurrentWorkspace.spec.js │ │ │ │ │ └── index.js │ │ │ │ ├── Theme/ │ │ │ │ │ └── index.js │ │ │ │ └── Toaster/ │ │ │ │ └── index.js │ │ │ ├── selectors/ │ │ │ │ └── tab.js │ │ │ ├── styles/ │ │ │ │ └── globals.css │ │ │ ├── test-utils/ │ │ │ │ └── mocks/ │ │ │ │ └── codemirror.js │ │ │ ├── themes/ │ │ │ │ ├── DesignSystem/ │ │ │ │ │ ├── DesignSystem.stories.jsx │ │ │ │ │ ├── Overview.jsx │ │ │ │ │ └── Theme.stories.jsx │ │ │ │ ├── PaletteViewer/ │ │ │ │ │ ├── Catppuccin.stories.jsx │ │ │ │ │ └── components.jsx │ │ │ │ ├── dark/ │ │ │ │ │ ├── catppuccin-frappe.js │ │ │ │ │ ├── catppuccin-macchiato.js │ │ │ │ │ ├── catppuccin-mocha.js │ │ │ │ │ ├── dark-monochrome.js │ │ │ │ │ ├── dark-pastel.js │ │ │ │ │ ├── dark.js │ │ │ │ │ ├── nord.js │ │ │ │ │ └── vscode.js │ │ │ │ ├── index.js │ │ │ │ ├── light/ │ │ │ │ │ ├── catppuccin-latte.js │ │ │ │ │ ├── light-monochrome.js │ │ │ │ │ ├── light-pastel.js │ │ │ │ │ ├── light.js │ │ │ │ │ └── vscode.js │ │ │ │ └── schema/ │ │ │ │ ├── index.js │ │ │ │ └── oss.js │ │ │ ├── ui/ │ │ │ │ ├── ActionIcon/ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ └── index.js │ │ │ │ ├── Button/ │ │ │ │ │ ├── Button.stories.jsx │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ └── index.js │ │ │ │ ├── ErrorBanner/ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ └── index.js │ │ │ │ ├── HeightBoundContainer/ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ └── index.js │ │ │ │ ├── MenuDropdown/ │ │ │ │ │ ├── SubMenuItem/ │ │ │ │ │ │ └── index.js │ │ │ │ │ └── index.js │ │ │ │ ├── MethodBadge/ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ └── index.js │ │ │ │ ├── ResponsiveTabs/ │ │ │ │ │ ├── StyledWrapper.js │ │ │ │ │ └── index.js │ │ │ │ └── StatusBadge/ │ │ │ │ ├── StyledWrapper.js │ │ │ │ └── index.js │ │ │ └── utils/ │ │ │ ├── auth/ │ │ │ │ ├── index.js │ │ │ │ └── index.spec.js │ │ │ ├── beta-features.js │ │ │ ├── bruno-clipboard.js │ │ │ ├── codegenerator/ │ │ │ │ ├── auth.js │ │ │ │ ├── har.js │ │ │ │ └── targets.js │ │ │ ├── codemirror/ │ │ │ │ ├── autocomplete.js │ │ │ │ ├── autocomplete.spec.js │ │ │ │ ├── autocompleteConstants.js │ │ │ │ ├── brunoVarInfo.js │ │ │ │ ├── brunoVarInfo.spec.js │ │ │ │ ├── javascript-lint.js │ │ │ │ ├── lang-detect.js │ │ │ │ ├── linkAware.js │ │ │ │ ├── linkAware.spec.js │ │ │ │ ├── lint-errors.js │ │ │ │ └── mock-data-hints.js │ │ │ ├── collections/ │ │ │ │ ├── emptyStateRequest.js │ │ │ │ ├── export.js │ │ │ │ ├── index.js │ │ │ │ ├── index.spec.js │ │ │ │ └── search.js │ │ │ ├── common/ │ │ │ │ ├── bulkKeyValueUtils.js │ │ │ │ ├── codemirror.js │ │ │ │ ├── constants.js │ │ │ │ ├── error.js │ │ │ │ ├── folders-requests-sorting.spec.js │ │ │ │ ├── format-response.spec.js │ │ │ │ ├── index.js │ │ │ │ ├── index.spec.js │ │ │ │ ├── ipc.js │ │ │ │ ├── masked-editor.js │ │ │ │ ├── path.js │ │ │ │ ├── path.spec.js │ │ │ │ ├── path.windows.spec.js │ │ │ │ ├── platform.js │ │ │ │ ├── regex.js │ │ │ │ ├── regex.spec.js │ │ │ │ └── setupPolyfills.js │ │ │ ├── curl/ │ │ │ │ ├── content-type.js │ │ │ │ ├── curl-to-json.js │ │ │ │ ├── curl-to-json.spec.js │ │ │ │ ├── index.js │ │ │ │ ├── parse-curl.js │ │ │ │ └── parse-curl.spec.js │ │ │ ├── environments.js │ │ │ ├── exporters/ │ │ │ │ ├── bruno-environment.js │ │ │ │ ├── openapi-spec.js │ │ │ │ ├── openapi-spec.spec.js │ │ │ │ ├── opencollection.js │ │ │ │ └── postman-collection.js │ │ │ ├── filesystem.js │ │ │ ├── git/ │ │ │ │ ├── index.js │ │ │ │ └── index.spec.js │ │ │ ├── idb/ │ │ │ │ └── index.js │ │ │ ├── importers/ │ │ │ │ ├── bruno-collection.js │ │ │ │ ├── bruno-environment.js │ │ │ │ ├── common.js │ │ │ │ ├── file-reader.js │ │ │ │ ├── insomnia-collection.js │ │ │ │ ├── openapi-collection.js │ │ │ │ ├── opencollection.js │ │ │ │ ├── postman-collection.js │ │ │ │ ├── postman-environment.js │ │ │ │ └── wsdl-collection.js │ │ │ ├── network/ │ │ │ │ ├── cancelTokens.js │ │ │ │ ├── grpc-event-listeners.js │ │ │ │ ├── index.js │ │ │ │ └── ws-event-listeners.js │ │ │ ├── response/ │ │ │ │ └── index.js │ │ │ ├── responseBodyProcessor.js │ │ │ ├── tabs/ │ │ │ │ └── index.js │ │ │ ├── terminal.js │ │ │ ├── tests/ │ │ │ │ └── collections/ │ │ │ │ ├── examples-export-import.spec.js │ │ │ │ ├── grpc-export-import.spec.js │ │ │ │ └── items-sequencing.spec.js │ │ │ ├── url/ │ │ │ │ ├── index.js │ │ │ │ └── index.spec.js │ │ │ └── workspaces/ │ │ │ └── index.js │ │ ├── storybook/ │ │ │ ├── main.js │ │ │ └── preview.jsx │ │ └── tailwind.config.js │ ├── bruno-cli/ │ │ ├── .gitignore │ │ ├── bin/ │ │ │ └── bru.js │ │ ├── changelog.md │ │ ├── examples/ │ │ │ ├── report.html │ │ │ └── report.json │ │ ├── license.md │ │ ├── package.json │ │ ├── readme.md │ │ ├── src/ │ │ │ ├── commands/ │ │ │ │ ├── import.js │ │ │ │ └── run.js │ │ │ ├── constants.js │ │ │ ├── index.js │ │ │ ├── reporters/ │ │ │ │ ├── html.js │ │ │ │ └── junit.js │ │ │ ├── runner/ │ │ │ │ ├── awsv4auth-helper.js │ │ │ │ ├── interpolate-string.js │ │ │ │ ├── interpolate-vars.js │ │ │ │ ├── prepare-request.js │ │ │ │ └── run-single-request.js │ │ │ ├── store/ │ │ │ │ └── tokenStore.js │ │ │ └── utils/ │ │ │ ├── axios-instance.js │ │ │ ├── bru.js │ │ │ ├── collection.js │ │ │ ├── common.js │ │ │ ├── cookies.js │ │ │ ├── environment.js │ │ │ ├── filesystem.js │ │ │ ├── form-data.js │ │ │ ├── oauth2.js │ │ │ ├── proxy-util.js │ │ │ ├── request.js │ │ │ ├── run.js │ │ │ └── sanitize-results.js │ │ └── tests/ │ │ ├── reporters/ │ │ │ ├── html.spec.js │ │ │ ├── junit.spec.js │ │ │ └── skip-body.spec.js │ │ ├── runner/ │ │ │ ├── collection-json-from-pathname.spec.js │ │ │ ├── fixtures/ │ │ │ │ ├── collection-json-from-pathname/ │ │ │ │ │ └── collection/ │ │ │ │ │ ├── bruno.json │ │ │ │ │ ├── collection.bru │ │ │ │ │ ├── folder_1/ │ │ │ │ │ │ ├── folder.bru │ │ │ │ │ │ ├── folder_1/ │ │ │ │ │ │ │ ├── folder.bru │ │ │ │ │ │ │ ├── request_1.bru │ │ │ │ │ │ │ ├── request_2.bru │ │ │ │ │ │ │ └── request_3.bru │ │ │ │ │ │ ├── folder_2/ │ │ │ │ │ │ │ ├── folder.bru │ │ │ │ │ │ │ ├── request_1.bru │ │ │ │ │ │ │ ├── request_2.bru │ │ │ │ │ │ │ └── request_3.bru │ │ │ │ │ │ ├── request_1.bru │ │ │ │ │ │ ├── request_2.bru │ │ │ │ │ │ └── request_3.bru │ │ │ │ │ ├── folder_2/ │ │ │ │ │ │ ├── folder.bru │ │ │ │ │ │ ├── request_1.bru │ │ │ │ │ │ ├── request_2.bru │ │ │ │ │ │ └── request_3.bru │ │ │ │ │ ├── request_1.bru │ │ │ │ │ ├── request_2.bru │ │ │ │ │ └── request_3.bru │ │ │ │ └── opencollection/ │ │ │ │ └── collection/ │ │ │ │ ├── environments/ │ │ │ │ │ └── dev.yml │ │ │ │ ├── get-users.yml │ │ │ │ ├── opencollection.yml │ │ │ │ └── users/ │ │ │ │ ├── create-user.yml │ │ │ │ └── folder.yml │ │ │ ├── prepare-request.spec.js │ │ │ └── report-metadata.spec.js │ │ └── utils/ │ │ ├── collection/ │ │ │ ├── create-collection-from-bruno-object.spec.js │ │ │ └── get-call-stack.spec.js │ │ ├── common.spec.js │ │ ├── filesystem.spec.js │ │ └── parse-environment-json.spec.js │ ├── bruno-common/ │ │ ├── .gitignore │ │ ├── babel.config.js │ │ ├── jest.config.js │ │ ├── license.md │ │ ├── package.json │ │ ├── readme.md │ │ ├── rollup.config.js │ │ ├── src/ │ │ │ ├── example-status/ │ │ │ │ └── index.ts │ │ │ ├── index.ts │ │ │ ├── interpolate/ │ │ │ │ ├── index.spec.ts │ │ │ │ └── index.ts │ │ │ ├── runner/ │ │ │ │ ├── index.ts │ │ │ │ ├── reports/ │ │ │ │ │ └── html/ │ │ │ │ │ ├── generate-report.ts │ │ │ │ │ └── template.ts │ │ │ │ ├── runner-summary.ts │ │ │ │ ├── types/ │ │ │ │ │ └── index.ts │ │ │ │ └── utils/ │ │ │ │ └── index.ts │ │ │ ├── tags/ │ │ │ │ ├── index.spec.ts │ │ │ │ └── index.ts │ │ │ ├── utils/ │ │ │ │ ├── faker-functions.spec.ts │ │ │ │ ├── faker-functions.ts │ │ │ │ ├── form-data.spec.ts │ │ │ │ ├── form-data.ts │ │ │ │ ├── index.ts │ │ │ │ ├── prompt-variables.spec.ts │ │ │ │ ├── prompt-variables.ts │ │ │ │ ├── template-hasher.spec.ts │ │ │ │ ├── template-hasher.ts │ │ │ │ └── url/ │ │ │ │ ├── index.spec.ts │ │ │ │ └── index.ts │ │ │ └── zoom/ │ │ │ └── index.ts │ │ └── tsconfig.json │ ├── bruno-converters/ │ │ ├── .gitignore │ │ ├── babel.config.js │ │ ├── jest.config.js │ │ ├── jest.setup.js │ │ ├── license.md │ │ ├── package.json │ │ ├── readme.md │ │ ├── rollup.config.js │ │ ├── src/ │ │ │ ├── common/ │ │ │ │ └── index.js │ │ │ ├── constants/ │ │ │ │ ├── index.js │ │ │ │ └── regex.js │ │ │ ├── index.js │ │ │ ├── insomnia/ │ │ │ │ ├── env-utils.js │ │ │ │ └── insomnia-to-bruno.js │ │ │ ├── openapi/ │ │ │ │ └── openapi-to-bruno.js │ │ │ ├── opencollection/ │ │ │ │ ├── bruno-to-opencollection.ts │ │ │ │ ├── common/ │ │ │ │ │ ├── actions.ts │ │ │ │ │ ├── assertions.ts │ │ │ │ │ ├── auth.ts │ │ │ │ │ ├── body.ts │ │ │ │ │ ├── headers.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── params.ts │ │ │ │ │ ├── scripts.ts │ │ │ │ │ └── variables.ts │ │ │ │ ├── environment.ts │ │ │ │ ├── folder.ts │ │ │ │ ├── index.ts │ │ │ │ ├── items/ │ │ │ │ │ ├── graphql.ts │ │ │ │ │ ├── grpc.ts │ │ │ │ │ ├── http.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── websocket.ts │ │ │ │ ├── opencollection-to-bruno.ts │ │ │ │ └── types.ts │ │ │ ├── postman/ │ │ │ │ ├── bruno-to-postman.js │ │ │ │ ├── postman-env-to-bruno-env.js │ │ │ │ ├── postman-to-bruno.js │ │ │ │ └── postman-translations.js │ │ │ ├── utils/ │ │ │ │ ├── ast-utils.js │ │ │ │ ├── bruno-send-request-transformer.js │ │ │ │ ├── bruno-to-postman-translator.js │ │ │ │ ├── flatten.js │ │ │ │ ├── postman-to-bruno-translator.js │ │ │ │ └── send-request-transformer.js │ │ │ ├── workers/ │ │ │ │ ├── postman-translator-worker.js │ │ │ │ └── scripts/ │ │ │ │ └── translate-postman-scripts.js │ │ │ └── wsdl/ │ │ │ └── wsdl-to-bruno.js │ │ ├── tests/ │ │ │ ├── bruno/ │ │ │ │ ├── bruno-to-postman-translations/ │ │ │ │ │ ├── cookies.test.js │ │ │ │ │ ├── environment.test.js │ │ │ │ │ ├── execution.test.js │ │ │ │ │ ├── request.test.js │ │ │ │ │ ├── response.test.js │ │ │ │ │ ├── send-request.test.js │ │ │ │ │ ├── testing-framework.test.js │ │ │ │ │ └── variables.test.js │ │ │ │ └── bruno-to-postman-with-tests.spec.js │ │ │ ├── common/ │ │ │ │ └── sanitizeTag.spec.js │ │ │ ├── insomnia/ │ │ │ │ ├── env-utils.spec.js │ │ │ │ ├── insomnia-collection-v5.spec.js │ │ │ │ └── insomnia-collection.spec.js │ │ │ ├── openapi/ │ │ │ │ ├── openapi-to-bruno/ │ │ │ │ │ ├── openapi-auth.spec.js │ │ │ │ │ ├── openapi-body.spec.js │ │ │ │ │ ├── openapi-circular-references.spec.js │ │ │ │ │ ├── openapi-import-grouping.spec.js │ │ │ │ │ ├── openapi-path-parameters.spec.js │ │ │ │ │ ├── openapi-server-variables.spec.js │ │ │ │ │ ├── openapi-tags.spec.js │ │ │ │ │ ├── openapi-to-bruno.spec.js │ │ │ │ │ └── path-based-grouping-duplicate-names.spec.js │ │ │ │ └── openapi-with-examples.spec.js │ │ │ ├── postman/ │ │ │ │ ├── bruno-to-postman-with-examples.spec.js │ │ │ │ ├── bruno-to-postman.spec.js │ │ │ │ ├── postman-env-to-bruno-env.spec.js │ │ │ │ ├── postman-to-bruno/ │ │ │ │ │ ├── collection-auth.spec.js │ │ │ │ │ ├── folder-auth.spec.js │ │ │ │ │ ├── postman-to-bruno.spec.js │ │ │ │ │ ├── postman-translations/ │ │ │ │ │ │ ├── postman-request.spec.js │ │ │ │ │ │ └── postman-response.spec.js │ │ │ │ │ ├── process-auth.spec.js │ │ │ │ │ ├── request-auth.spec.js │ │ │ │ │ └── transform-description.spec.js │ │ │ │ └── postman-translations/ │ │ │ │ ├── postman-comments.spec.js │ │ │ │ ├── postman-cookie-conversions.spec.js │ │ │ │ ├── postman-edge-cases.spec.js │ │ │ │ ├── postman-test-commands.spec.js │ │ │ │ ├── postman-variables.spec.js │ │ │ │ └── transpiler-tests/ │ │ │ │ ├── combined.test.js │ │ │ │ ├── environment.test.js │ │ │ │ ├── exec-flow.test.js │ │ │ │ ├── legacy-global-apis.test.js │ │ │ │ ├── legacy-tests-syntax.test.js │ │ │ │ ├── multiline-syntax.test.js │ │ │ │ ├── postman-references.test.js │ │ │ │ ├── request.test.js │ │ │ │ ├── response.test.js │ │ │ │ ├── scoped-variables.test.js │ │ │ │ ├── testing-framework.test.js │ │ │ │ ├── transformers/ │ │ │ │ │ └── send-request.test.js │ │ │ │ ├── variable-chaining.test.js │ │ │ │ └── variables.test.js │ │ │ ├── postman-with-examples.spec.js │ │ │ ├── utils/ │ │ │ │ ├── flatten.spec.js │ │ │ │ └── getMemberExpressionString.test.js │ │ │ └── wsdl/ │ │ │ └── wsdl-to-bruno.spec.js │ │ ├── tsconfig.json │ │ └── types/ │ │ └── common.d.ts │ ├── bruno-docs/ │ │ ├── package.json │ │ └── readme.md │ ├── bruno-electron/ │ │ ├── .gitignore │ │ ├── electron-builder-config.js │ │ ├── notarize.js │ │ ├── package.json │ │ ├── readme.md │ │ ├── resources/ │ │ │ ├── data/ │ │ │ │ └── sample-collection.json │ │ │ ├── entitlements.mac.plist │ │ │ └── icons/ │ │ │ └── mac/ │ │ │ └── icon.icns │ │ ├── src/ │ │ │ ├── about/ │ │ │ │ └── about.css │ │ │ ├── app/ │ │ │ │ ├── about-bruno.js │ │ │ │ ├── apiSpecs.js │ │ │ │ ├── apiSpecsWatcher.js │ │ │ │ ├── collection-watcher.js │ │ │ │ ├── collections.js │ │ │ │ ├── dotenv-watcher.js │ │ │ │ ├── menu-template.js │ │ │ │ ├── onboarding.js │ │ │ │ ├── system-monitor.js │ │ │ │ └── workspace-watcher.js │ │ │ ├── cache/ │ │ │ │ ├── apiSpecUids.js │ │ │ │ └── requestUids.js │ │ │ ├── index.js │ │ │ ├── ipc/ │ │ │ │ ├── apiSpec.js │ │ │ │ ├── collection.js │ │ │ │ ├── filesystem.js │ │ │ │ ├── git.js │ │ │ │ ├── global-environments.js │ │ │ │ ├── network/ │ │ │ │ │ ├── authorize-user-in-system-browser.js │ │ │ │ │ ├── authorize-user-in-window.js │ │ │ │ │ ├── awsv4auth-helper.js │ │ │ │ │ ├── axios-instance.js │ │ │ │ │ ├── cert-utils.js │ │ │ │ │ ├── grpc-event-handlers.js │ │ │ │ │ ├── index.js │ │ │ │ │ ├── interpolate-string.js │ │ │ │ │ ├── interpolate-vars.js │ │ │ │ │ ├── prepare-gql-introspection-request.js │ │ │ │ │ ├── prepare-grpc-request.js │ │ │ │ │ ├── prepare-request.js │ │ │ │ │ └── ws-event-handlers.js │ │ │ │ ├── notifications.js │ │ │ │ ├── openapi-sync.js │ │ │ │ ├── preferences.js │ │ │ │ ├── system-monitor.js │ │ │ │ ├── terminal.js │ │ │ │ └── workspace.js │ │ │ ├── preload.js │ │ │ ├── store/ │ │ │ │ ├── bruno-config.js │ │ │ │ ├── collection-security.js │ │ │ │ ├── cookies.js │ │ │ │ ├── default-workspace.js │ │ │ │ ├── env-secrets.js │ │ │ │ ├── global-environments.js │ │ │ │ ├── last-opened-collections.js │ │ │ │ ├── last-opened-workspaces.js │ │ │ │ ├── oauth2.js │ │ │ │ ├── preferences.js │ │ │ │ ├── process-env.js │ │ │ │ ├── system-proxy.js │ │ │ │ ├── ui-state-snapshot.js │ │ │ │ ├── window-state.js │ │ │ │ └── workspace-environments.js │ │ │ └── utils/ │ │ │ ├── arch.js │ │ │ ├── cancel-token.js │ │ │ ├── collection-import.js │ │ │ ├── collection.js │ │ │ ├── common.js │ │ │ ├── constants.js │ │ │ ├── cookies.js │ │ │ ├── deeplink.js │ │ │ ├── default-location.js │ │ │ ├── encryption.js │ │ │ ├── filesystem.js │ │ │ ├── filesystem.test.js │ │ │ ├── form-data.js │ │ │ ├── git.js │ │ │ ├── oauth2-protocol-handler.js │ │ │ ├── oauth2.js │ │ │ ├── parse.js │ │ │ ├── proxy-util.js │ │ │ ├── tests/ │ │ │ │ ├── collection-utils.spec.js │ │ │ │ ├── filesystem/ │ │ │ │ │ └── index.spec.js │ │ │ │ └── fixtures/ │ │ │ │ └── filesystem/ │ │ │ │ └── copypath-removepath.js │ │ │ ├── transformBrunoConfig.js │ │ │ ├── window.js │ │ │ ├── workspace-config.js │ │ │ └── workspace-lock.js │ │ └── tests/ │ │ ├── cookies-store.test.js │ │ ├── network/ │ │ │ ├── authorize-user.spec.js │ │ │ ├── execute-request-error-handler.spec.js │ │ │ ├── fetch-gql-schema-handler.spec.js │ │ │ ├── index.spec.js │ │ │ ├── interpolate-vars.spec.js │ │ │ ├── prepare-gql-introspection-request.spec.js │ │ │ ├── prepare-grpc-request.spec.js │ │ │ ├── prepare-request.spec.js │ │ │ └── prepare-ws-request.spec.js │ │ ├── prepare-request.test.js │ │ ├── store/ │ │ │ ├── default-location-migration.spec.js │ │ │ ├── global-environments.test.js │ │ │ └── proxy-preferences.spec.js │ │ └── utils/ │ │ ├── collection.spec.js │ │ ├── common.spec.js │ │ ├── encryption.spec.js │ │ ├── form-data.spec.js │ │ ├── proxy-util.spec.js │ │ ├── transform-bruno-config.spec.js │ │ └── workspace-config.spec.js │ ├── bruno-filestore/ │ │ ├── .gitignore │ │ ├── LICENSE.md │ │ ├── README.md │ │ ├── babel.config.js │ │ ├── jest.config.js │ │ ├── package.json │ │ ├── rollup.config.js │ │ ├── src/ │ │ │ ├── constants.ts │ │ │ ├── formats/ │ │ │ │ ├── bru/ │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── tests/ │ │ │ │ │ │ ├── fixtures/ │ │ │ │ │ │ │ ├── oauth2-additional-params.js │ │ │ │ │ │ │ └── request-parse-and-redact-body-data/ │ │ │ │ │ │ │ ├── input.bru │ │ │ │ │ │ │ └── output.bru │ │ │ │ │ │ ├── oauth2-additional-params.spec.js │ │ │ │ │ │ └── request-parse-and-redact-body-data.spec.js │ │ │ │ │ └── utils/ │ │ │ │ │ ├── oauth2-additional-params.ts │ │ │ │ │ └── request-parse-and-redact-body-data.ts │ │ │ │ └── yml/ │ │ │ │ ├── common/ │ │ │ │ │ ├── actions.ts │ │ │ │ │ ├── assertions.ts │ │ │ │ │ ├── auth-oauth2.ts │ │ │ │ │ ├── auth.ts │ │ │ │ │ ├── body.ts │ │ │ │ │ ├── headers.ts │ │ │ │ │ ├── params.ts │ │ │ │ │ ├── scripts.ts │ │ │ │ │ └── variables.ts │ │ │ │ ├── index.ts │ │ │ │ ├── items/ │ │ │ │ │ ├── parseGraphQLRequest.ts │ │ │ │ │ ├── parseGrpcRequest.ts │ │ │ │ │ ├── parseHttpRequest.ts │ │ │ │ │ ├── parseScript.ts │ │ │ │ │ ├── parseWebsocketRequest.ts │ │ │ │ │ ├── stringifyGraphQLRequest.ts │ │ │ │ │ ├── stringifyGrpcRequest.ts │ │ │ │ │ ├── stringifyHttpRequest.ts │ │ │ │ │ ├── stringifyScript.ts │ │ │ │ │ └── stringifyWebsocketRequest.ts │ │ │ │ ├── parseCollection.ts │ │ │ │ ├── parseEnvironment.ts │ │ │ │ ├── parseFolder.ts │ │ │ │ ├── parseItem.ts │ │ │ │ ├── stringifyCollection.ts │ │ │ │ ├── stringifyEnvironment.ts │ │ │ │ ├── stringifyFolder.ts │ │ │ │ ├── stringifyItem.ts │ │ │ │ └── utils.ts │ │ │ ├── index.ts │ │ │ ├── types/ │ │ │ │ └── bruno-lang.d.ts │ │ │ ├── types.ts │ │ │ ├── utils/ │ │ │ │ └── index.ts │ │ │ └── workers/ │ │ │ ├── WorkerQueue/ │ │ │ │ └── index.ts │ │ │ ├── index.ts │ │ │ └── worker-script.ts │ │ ├── test-results/ │ │ │ └── .last-run.json │ │ └── tsconfig.json │ ├── bruno-graphql-docs/ │ │ ├── .gitignore │ │ ├── license.md │ │ ├── package.json │ │ ├── readme.md │ │ ├── rollup.config.js │ │ ├── src/ │ │ │ ├── components/ │ │ │ │ ├── DocExplorer/ │ │ │ │ │ ├── Argument.tsx │ │ │ │ │ ├── DefaultValue.tsx │ │ │ │ │ ├── Directive.tsx │ │ │ │ │ ├── FieldDoc.tsx │ │ │ │ │ ├── MarkdownContent.tsx │ │ │ │ │ ├── SchemaDoc.tsx │ │ │ │ │ ├── SearchBox.tsx │ │ │ │ │ ├── SearchResults.tsx │ │ │ │ │ ├── TypeDoc.tsx │ │ │ │ │ ├── TypeLink.tsx │ │ │ │ │ └── types.ts │ │ │ │ └── DocExplorer.tsx │ │ │ ├── index.css │ │ │ ├── index.ts │ │ │ └── utility/ │ │ │ └── debounce.ts │ │ └── tsconfig.json │ ├── bruno-js/ │ │ ├── .gitignore │ │ ├── license.md │ │ ├── package.json │ │ ├── readme.md │ │ ├── src/ │ │ │ ├── bru.js │ │ │ ├── bruno-request.js │ │ │ ├── bruno-response.js │ │ │ ├── index.js │ │ │ ├── interpolate-string.js │ │ │ ├── runtime/ │ │ │ │ ├── assert-runtime.js │ │ │ │ ├── script-runtime.js │ │ │ │ ├── test-runtime.js │ │ │ │ └── vars-runtime.js │ │ │ ├── sandbox/ │ │ │ │ ├── bundle-libraries.js │ │ │ │ ├── mixins/ │ │ │ │ │ └── typed-arrays.js │ │ │ │ ├── node-vm/ │ │ │ │ │ ├── cjs-loader.js │ │ │ │ │ ├── console.js │ │ │ │ │ ├── constants.js │ │ │ │ │ ├── index.js │ │ │ │ │ ├── index.spec.js │ │ │ │ │ └── utils.js │ │ │ │ └── quickjs/ │ │ │ │ ├── index.js │ │ │ │ ├── shims/ │ │ │ │ │ ├── bru.js │ │ │ │ │ ├── bruno-request.js │ │ │ │ │ ├── bruno-response.js │ │ │ │ │ ├── console.js │ │ │ │ │ ├── lib/ │ │ │ │ │ │ ├── axios.js │ │ │ │ │ │ ├── axios.spec.js │ │ │ │ │ │ ├── crypto-utils.js │ │ │ │ │ │ ├── crypto-utils.spec.js │ │ │ │ │ │ ├── index.js │ │ │ │ │ │ ├── jwt.js │ │ │ │ │ │ ├── nanoid.js │ │ │ │ │ │ ├── path.js │ │ │ │ │ │ ├── utils.js │ │ │ │ │ │ └── uuid.js │ │ │ │ │ ├── local-module.js │ │ │ │ │ └── test.js │ │ │ │ └── utils/ │ │ │ │ └── index.js │ │ │ ├── test-results.js │ │ │ ├── test.js │ │ │ ├── utils/ │ │ │ │ ├── error-formatter.js │ │ │ │ ├── error-formatter.spec.js │ │ │ │ ├── results.js │ │ │ │ └── sandbox.js │ │ │ └── utils.js │ │ └── tests/ │ │ ├── bruno-request-delete-header.spec.js │ │ ├── runtime.spec.js │ │ ├── setEnvVar.spec.js │ │ └── utils.spec.js │ ├── bruno-lang/ │ │ ├── .gitignore │ │ ├── example/ │ │ │ ├── request.bru │ │ │ ├── request.json │ │ │ └── request.next.bru │ │ ├── license.md │ │ ├── package.json │ │ ├── readme.md │ │ ├── src/ │ │ │ └── index.js │ │ ├── v1/ │ │ │ ├── src/ │ │ │ │ ├── body-tag.js │ │ │ │ ├── env-vars-tag.js │ │ │ │ ├── headers-tag.js │ │ │ │ ├── index.js │ │ │ │ ├── inline-tag.js │ │ │ │ ├── key-val-lines.js │ │ │ │ ├── params-tag.js │ │ │ │ ├── script-tag.js │ │ │ │ ├── tests-tag.js │ │ │ │ └── utils.js │ │ │ └── tests/ │ │ │ ├── body-tag.spec.js │ │ │ ├── bru-to-env-json.spec.js │ │ │ ├── bru-to-json.spec.js │ │ │ ├── env-json-to-bru.spec.js │ │ │ ├── fixtures/ │ │ │ │ ├── env.bru │ │ │ │ └── request.bru │ │ │ ├── inline-tag.spec.js │ │ │ ├── json-to-bru.spec.js │ │ │ ├── key-val-lines.spec.js │ │ │ ├── script-tag.spec.js │ │ │ ├── tests-tag.spec.js │ │ │ └── utils.spec.js │ │ └── v2/ │ │ ├── src/ │ │ │ ├── bruToJson.js │ │ │ ├── collectionBruToJson.js │ │ │ ├── common/ │ │ │ │ ├── attributes.js │ │ │ │ └── semantic-utils.js │ │ │ ├── dotenvToJson.js │ │ │ ├── envToJson.js │ │ │ ├── example/ │ │ │ │ ├── bruToJson.js │ │ │ │ ├── jsonToBru.js │ │ │ │ ├── request/ │ │ │ │ │ └── bruToJson.js │ │ │ │ └── response/ │ │ │ │ └── bruToJson.js │ │ │ ├── jsonToBru.js │ │ │ ├── jsonToCollectionBru.js │ │ │ ├── jsonToEnv.js │ │ │ └── utils.js │ │ └── tests/ │ │ ├── assert.spec.js │ │ ├── bruToJson.spec.js │ │ ├── collection.spec.js │ │ ├── custom-methods/ │ │ │ ├── custom-method.spec.js │ │ │ └── fixtures/ │ │ │ ├── custom-method-with-special-chars.bru │ │ │ ├── custom-method-with-special-chars.json │ │ │ ├── custom-method-x-custom.bru │ │ │ ├── custom-method-x-custom.json │ │ │ ├── custom-method.bru │ │ │ └── custom-method.json │ │ ├── defaults.spec.js │ │ ├── dictionary.spec.js │ │ ├── dotenvToJson.spec.js │ │ ├── envToJson.spec.js │ │ ├── examples/ │ │ │ ├── examples.spec.js │ │ │ └── fixtures/ │ │ │ ├── bru/ │ │ │ │ ├── bruToJson-empty-example.bru │ │ │ │ ├── bruToJson-json-body.bru │ │ │ │ ├── bruToJson-multiple-examples.bru │ │ │ │ ├── bruToJson-no-examples.bru │ │ │ │ ├── bruToJson-response-example.bru │ │ │ │ ├── bruToJson-single-example.bru │ │ │ │ ├── bruToJson-text-body.bru │ │ │ │ ├── bruToJson-xml-body.bru │ │ │ │ ├── complex-with-auth.bru │ │ │ │ ├── examples-complex.bru │ │ │ │ ├── examples-multiline-contenttype.bru │ │ │ │ ├── examples-multiline-description.bru │ │ │ │ ├── examples-simple.bru │ │ │ │ ├── form-data-complex.bru │ │ │ │ ├── jsonToBru-bodytypes.bru │ │ │ │ ├── jsonToBru-multiple.bru │ │ │ │ ├── jsonToBru-response.bru │ │ │ │ ├── jsonToBru-simple.bru │ │ │ │ ├── multiple-examples-variations.bru │ │ │ │ └── oauth2-examples.bru │ │ │ └── json/ │ │ │ ├── bruToJson-empty-example.json │ │ │ ├── bruToJson-json-body.json │ │ │ ├── bruToJson-multiple-examples.json │ │ │ ├── bruToJson-no-examples.json │ │ │ ├── bruToJson-response-example.json │ │ │ ├── bruToJson-single-example.json │ │ │ ├── bruToJson-text-body.json │ │ │ ├── bruToJson-xml-body.json │ │ │ ├── complex-with-auth.json │ │ │ ├── examples-complex.json │ │ │ ├── examples-multiline-contenttype.json │ │ │ ├── examples-multiline-description.json │ │ │ ├── examples-simple.json │ │ │ ├── form-data-complex.json │ │ │ ├── jsonToBru-bodytypes.json │ │ │ ├── jsonToBru-multiple.json │ │ │ ├── jsonToBru-response.json │ │ │ ├── jsonToBru-simple.json │ │ │ ├── multiple-examples-variations.json │ │ │ └── oauth2-examples.json │ │ ├── fixtures/ │ │ │ ├── collection.bru │ │ │ ├── collection.json │ │ │ ├── request.bru │ │ │ └── request.json │ │ ├── getKeyString.spec.js │ │ ├── index.spec.js │ │ ├── jsonToBru.spec.js │ │ ├── jsonToEnv.spec.js │ │ ├── list.spec.js │ │ ├── oauth2-additional-params.spec.js │ │ ├── script.spec.js │ │ ├── settings/ │ │ │ ├── fixtures/ │ │ │ │ ├── settings-all-options.bru │ │ │ │ ├── settings-all-options.json │ │ │ │ ├── settings-minimal.bru │ │ │ │ └── settings-minimal.json │ │ │ └── settings.spec.js │ │ ├── tags.spec.js │ │ └── utils.spec.js │ ├── bruno-query/ │ │ ├── .gitignore │ │ ├── jest.config.js │ │ ├── license.md │ │ ├── package.json │ │ ├── readme.md │ │ ├── rollup.config.js │ │ ├── src/ │ │ │ └── index.ts │ │ ├── tests/ │ │ │ └── index.spec.ts │ │ └── tsconfig.json │ ├── bruno-requests/ │ │ ├── .gitignore │ │ ├── babel.config.js │ │ ├── jest.config.js │ │ ├── package.json │ │ ├── rollup.config.js │ │ ├── src/ │ │ │ ├── auth/ │ │ │ │ ├── digestauth-helper.js │ │ │ │ ├── digestauth-helper.spec.js │ │ │ │ ├── index.ts │ │ │ │ ├── oauth2-helper.spec.ts │ │ │ │ └── oauth2-helper.ts │ │ │ ├── cookies/ │ │ │ │ ├── index.spec.ts │ │ │ │ └── index.ts │ │ │ ├── grpc/ │ │ │ │ ├── grpc-client.js │ │ │ │ ├── grpc-client.spec.js │ │ │ │ ├── grpcMessageGenerator.js │ │ │ │ └── index.ts │ │ │ ├── index.ts │ │ │ ├── network/ │ │ │ │ ├── axios-instance.ts │ │ │ │ ├── index.ts │ │ │ │ └── system-proxy/ │ │ │ │ ├── index.spec.js │ │ │ │ ├── index.ts │ │ │ │ ├── types.ts │ │ │ │ └── utils/ │ │ │ │ ├── common.spec.ts │ │ │ │ ├── common.ts │ │ │ │ ├── linux.spec.ts │ │ │ │ ├── linux.ts │ │ │ │ ├── macos.spec.ts │ │ │ │ ├── macos.ts │ │ │ │ ├── windows.spec.ts │ │ │ │ └── windows.ts │ │ │ ├── scripting/ │ │ │ │ ├── index.ts │ │ │ │ ├── send-request.spec.ts │ │ │ │ └── send-request.ts │ │ │ ├── utils/ │ │ │ │ ├── agent-cache.spec.ts │ │ │ │ ├── agent-cache.ts │ │ │ │ ├── ca-cert.ts │ │ │ │ ├── http-https-agents.ts │ │ │ │ ├── node-vault.spec.ts │ │ │ │ ├── node-vault.ts │ │ │ │ ├── proxy-util.spec.ts │ │ │ │ ├── proxy-util.ts │ │ │ │ ├── shell-env.spec.ts │ │ │ │ ├── shell-env.ts │ │ │ │ ├── timeline-agent.ts │ │ │ │ ├── url-validation.spec.ts │ │ │ │ └── url-validation.ts │ │ │ └── ws/ │ │ │ ├── ws-client.js │ │ │ ├── ws-url.js │ │ │ └── ws-url.spec.ts │ │ └── tsconfig.json │ ├── bruno-schema/ │ │ ├── .gitignore │ │ ├── license.md │ │ ├── package.json │ │ ├── readme.md │ │ └── src/ │ │ ├── collections/ │ │ │ ├── index.js │ │ │ ├── index.spec.js │ │ │ ├── itemSchema.spec.js │ │ │ └── requestSchema.spec.js │ │ ├── common/ │ │ │ └── index.js │ │ ├── index.js │ │ └── utils/ │ │ └── testUtils.js │ ├── bruno-schema-types/ │ │ ├── .gitignore │ │ ├── package.json │ │ ├── src/ │ │ │ ├── collection/ │ │ │ │ ├── collection.ts │ │ │ │ ├── environment.ts │ │ │ │ ├── examples.ts │ │ │ │ ├── folder.ts │ │ │ │ ├── index.ts │ │ │ │ └── item.ts │ │ │ ├── common/ │ │ │ │ ├── auth.ts │ │ │ │ ├── file.ts │ │ │ │ ├── graphql.ts │ │ │ │ ├── index.ts │ │ │ │ ├── key-value.ts │ │ │ │ ├── multipart-form.ts │ │ │ │ ├── scripts.ts │ │ │ │ ├── uid.ts │ │ │ │ └── variables.ts │ │ │ ├── index.ts │ │ │ └── requests/ │ │ │ ├── grpc.ts │ │ │ ├── http.ts │ │ │ ├── index.ts │ │ │ └── websocket.ts │ │ └── tsconfig.json │ ├── bruno-tests/ │ │ ├── .gitignore │ │ ├── .nvmrc │ │ ├── additional-context-root-lib/ │ │ │ ├── index.js │ │ │ └── lib.js │ │ ├── collection/ │ │ │ ├── .env │ │ │ ├── .gitignore │ │ │ ├── .nvmrc │ │ │ ├── asserts/ │ │ │ │ └── test-assert-combinations.bru │ │ │ ├── auth/ │ │ │ │ ├── basic/ │ │ │ │ │ ├── via auth/ │ │ │ │ │ │ ├── Basic Auth 200.bru │ │ │ │ │ │ └── Basic Auth 401.bru │ │ │ │ │ └── via script/ │ │ │ │ │ ├── Basic Auth 200.bru │ │ │ │ │ └── Basic Auth 401.bru │ │ │ │ ├── bearer/ │ │ │ │ │ ├── via auth/ │ │ │ │ │ │ ├── Bearer Auth 200.bru │ │ │ │ │ │ └── Bearer Auth undefined.bru │ │ │ │ │ └── via headers/ │ │ │ │ │ └── Bearer Auth 200.bru │ │ │ │ ├── cookie/ │ │ │ │ │ ├── Check.bru │ │ │ │ │ └── Login.bru │ │ │ │ ├── digest/ │ │ │ │ │ ├── Digest Auth 200.bru │ │ │ │ │ ├── Digest Auth 401.bru │ │ │ │ │ └── folder.bru │ │ │ │ └── inherit auth/ │ │ │ │ └── inherit Bearer Auth 200.bru │ │ │ ├── bruno.json │ │ │ ├── collection.bru │ │ │ ├── echo/ │ │ │ │ ├── echo bom json.bru │ │ │ │ ├── echo form-url-encoded.bru │ │ │ │ ├── echo headers.bru │ │ │ │ ├── echo json.bru │ │ │ │ ├── echo multipart scripting.bru │ │ │ │ ├── echo multipart.bru │ │ │ │ ├── echo numbers.bru │ │ │ │ ├── echo plaintext.bru │ │ │ │ ├── echo xml parsed(self closing tags).bru │ │ │ │ ├── echo xml parsed.bru │ │ │ │ ├── echo xml raw.bru │ │ │ │ ├── multiline/ │ │ │ │ │ └── echo binary.bru │ │ │ │ ├── test echo any.bru │ │ │ │ └── test echo-any json.bru │ │ │ ├── environments/ │ │ │ │ ├── Local.bru │ │ │ │ └── Prod.bru │ │ │ ├── file.json │ │ │ ├── file.txt │ │ │ ├── graphql/ │ │ │ │ ├── mutation.bru │ │ │ │ ├── spacex.bru │ │ │ │ └── variable-interpolation.bru │ │ │ ├── lib/ │ │ │ │ ├── constants.js │ │ │ │ ├── math.js │ │ │ │ └── notes.js │ │ │ ├── package.json │ │ │ ├── ping.bru │ │ │ ├── preview/ │ │ │ │ ├── html/ │ │ │ │ │ └── bruno.bru │ │ │ │ └── image/ │ │ │ │ └── bruno.bru │ │ │ ├── readme.md │ │ │ ├── redirects/ │ │ │ │ ├── Disable Redirect.bru │ │ │ │ ├── Test Multipart Redirect Consumed FormData.bru │ │ │ │ ├── Test Multipart Redirect Multiple Fields.bru │ │ │ │ ├── Test Multipart Redirect.bru │ │ │ │ └── Test Redirect.bru │ │ │ ├── request-setting/ │ │ │ │ ├── folder.bru │ │ │ │ ├── follow-redirect.bru │ │ │ │ └── max-redirect.bru │ │ │ ├── response-parsing/ │ │ │ │ ├── test JSON false response.bru │ │ │ │ ├── test JSON null response.bru │ │ │ │ ├── test JSON number response.bru │ │ │ │ ├── test JSON response.bru │ │ │ │ ├── test JSON string response.bru │ │ │ │ ├── test JSON string with quotes response.bru │ │ │ │ ├── test JSON true response.bru │ │ │ │ ├── test JSON unsafe-int response.bru │ │ │ │ ├── test binary response.bru │ │ │ │ ├── test html response.bru │ │ │ │ ├── test image response.bru │ │ │ │ ├── test invalid JSON response with formatting.bru │ │ │ │ ├── test plain text response with formatting.bru │ │ │ │ ├── test plain text response.bru │ │ │ │ ├── test plain text utf16 response.bru │ │ │ │ ├── test plain text utf16-be with BOM response.bru │ │ │ │ ├── test plain text utf16-le with BOM response.bru │ │ │ │ ├── test plain text utf8 with BOM response.bru │ │ │ │ └── test xml response.bru │ │ │ ├── scripting/ │ │ │ │ ├── api/ │ │ │ │ │ ├── bru/ │ │ │ │ │ │ ├── cookies/ │ │ │ │ │ │ │ ├── Redirect Cookie Save.bru │ │ │ │ │ │ │ ├── clear.bru │ │ │ │ │ │ │ ├── deleteCookie.bru │ │ │ │ │ │ │ ├── deleteCookies.bru │ │ │ │ │ │ │ ├── folder.bru │ │ │ │ │ │ │ ├── getCookie.bru │ │ │ │ │ │ │ ├── getCookies.bru │ │ │ │ │ │ │ ├── hasCookie.bru │ │ │ │ │ │ │ ├── setCookie.bru │ │ │ │ │ │ │ ├── setCookieHeader.bru │ │ │ │ │ │ │ └── setCookies.bru │ │ │ │ │ │ ├── deleteAllCollectionVars.bru │ │ │ │ │ │ ├── deleteAllEnvVars.bru │ │ │ │ │ │ ├── deleteAllGlobalEnvVars.bru │ │ │ │ │ │ ├── deleteCollectionVar.bru │ │ │ │ │ │ ├── deleteGlobalEnvVar.bru │ │ │ │ │ │ ├── folder.bru │ │ │ │ │ │ ├── getAllCollectionVars.bru │ │ │ │ │ │ ├── getAllEnvVars.bru │ │ │ │ │ │ ├── getAllGlobalEnvVars.bru │ │ │ │ │ │ ├── getAllVars.bru │ │ │ │ │ │ ├── getCollectionName.bru │ │ │ │ │ │ ├── getCollectionVar.bru │ │ │ │ │ │ ├── getEnvName.bru │ │ │ │ │ │ ├── getEnvVar.bru │ │ │ │ │ │ ├── getFolderVar.bru │ │ │ │ │ │ ├── getProcessEnv.bru │ │ │ │ │ │ ├── getRequestVar.bru │ │ │ │ │ │ ├── getVar.bru │ │ │ │ │ │ ├── hasCollectionVar.bru │ │ │ │ │ │ ├── interpolate.bru │ │ │ │ │ │ ├── isSafeMode.bru │ │ │ │ │ │ ├── runRequest-1.bru │ │ │ │ │ │ ├── runRequest-2.bru │ │ │ │ │ │ ├── runRequest.bru │ │ │ │ │ │ ├── runner/ │ │ │ │ │ │ │ ├── 1.bru │ │ │ │ │ │ │ ├── 2.bru │ │ │ │ │ │ │ └── 3.bru │ │ │ │ │ │ ├── send-request/ │ │ │ │ │ │ │ ├── folder.bru │ │ │ │ │ │ │ ├── get-url-string.bru │ │ │ │ │ │ │ └── usage-patterns.bru │ │ │ │ │ │ ├── setCollectionVar.bru │ │ │ │ │ │ ├── setEnvVar.bru │ │ │ │ │ │ └── setVar.bru │ │ │ │ │ ├── req/ │ │ │ │ │ │ ├── deleteHeader.bru │ │ │ │ │ │ ├── deleteHeaders.bru │ │ │ │ │ │ ├── getBody.bru │ │ │ │ │ │ ├── getHeader.bru │ │ │ │ │ │ ├── getHeaders.bru │ │ │ │ │ │ ├── getHost.bru │ │ │ │ │ │ ├── getMethod.bru │ │ │ │ │ │ ├── getName.bru │ │ │ │ │ │ ├── getPath.bru │ │ │ │ │ │ ├── getPathParams.bru │ │ │ │ │ │ ├── getQueryString.bru │ │ │ │ │ │ ├── getTags.bru │ │ │ │ │ │ ├── getUrl.bru │ │ │ │ │ │ ├── setBody/ │ │ │ │ │ │ │ └── form-urlencoded/ │ │ │ │ │ │ │ ├── array body.bru │ │ │ │ │ │ │ ├── content-type via setHeader.bru │ │ │ │ │ │ │ ├── folder.bru │ │ │ │ │ │ │ ├── object body.bru │ │ │ │ │ │ │ └── string body.bru │ │ │ │ │ │ ├── setBody.bru │ │ │ │ │ │ ├── setHeader.bru │ │ │ │ │ │ ├── setHeaders.bru │ │ │ │ │ │ ├── setMethod.bru │ │ │ │ │ │ └── setUrl.bru │ │ │ │ │ └── res/ │ │ │ │ │ ├── getBody.bru │ │ │ │ │ ├── getHeader.bru │ │ │ │ │ ├── getHeaders.bru │ │ │ │ │ ├── getResponseTime.bru │ │ │ │ │ ├── getSize.bru │ │ │ │ │ ├── getStatus.bru │ │ │ │ │ ├── getStatusText.bru │ │ │ │ │ ├── getUrl.bru │ │ │ │ │ └── setBody/ │ │ │ │ │ ├── array.bru │ │ │ │ │ ├── boolean.bru │ │ │ │ │ ├── folder.bru │ │ │ │ │ ├── isJson after setBody.bru │ │ │ │ │ ├── null.bru │ │ │ │ │ ├── number.bru │ │ │ │ │ ├── object.bru │ │ │ │ │ ├── string.bru │ │ │ │ │ └── undefined.bru │ │ │ │ ├── inbuilt modules/ │ │ │ │ │ ├── axios/ │ │ │ │ │ │ └── axios-pre-req-script.bru │ │ │ │ │ ├── cheerio/ │ │ │ │ │ │ └── cheerio.bru │ │ │ │ │ ├── crypto-js/ │ │ │ │ │ │ └── crypto-js-pre-request-script.bru │ │ │ │ │ ├── crypto-utils/ │ │ │ │ │ │ ├── getRandomValues.bru │ │ │ │ │ │ └── randomBytes.bru │ │ │ │ │ ├── nanoid/ │ │ │ │ │ │ └── nanoid.bru │ │ │ │ │ ├── tv4/ │ │ │ │ │ │ ├── folder.bru │ │ │ │ │ │ └── tv4.bru │ │ │ │ │ ├── utils.js │ │ │ │ │ ├── uuid/ │ │ │ │ │ │ └── uuid.bru │ │ │ │ │ └── xml2js/ │ │ │ │ │ └── xml2js.bru │ │ │ │ ├── js/ │ │ │ │ │ ├── data types - request vars.bru │ │ │ │ │ ├── data types.bru │ │ │ │ │ ├── folder-collection script-tests pre.bru │ │ │ │ │ ├── folder-collection script-tests.bru │ │ │ │ │ ├── folder.bru │ │ │ │ │ └── setTimeout.bru │ │ │ │ ├── local modules/ │ │ │ │ │ ├── additional context root.bru │ │ │ │ │ ├── invalid and valid module imports.bru │ │ │ │ │ ├── sum (without js extn).bru │ │ │ │ │ └── sum.bru │ │ │ │ ├── node-builtins/ │ │ │ │ │ ├── buffer.bru │ │ │ │ │ ├── encoding.bru │ │ │ │ │ ├── events.bru │ │ │ │ │ ├── fetch-api.bru │ │ │ │ │ ├── intl.bru │ │ │ │ │ ├── json.bru │ │ │ │ │ ├── node-crypto.bru │ │ │ │ │ ├── node-fs.bru │ │ │ │ │ ├── node-os.bru │ │ │ │ │ ├── node-path.bru │ │ │ │ │ ├── node-querystring.bru │ │ │ │ │ ├── node-stream.bru │ │ │ │ │ ├── node-util.bru │ │ │ │ │ ├── node-zlib.bru │ │ │ │ │ ├── process.bru │ │ │ │ │ ├── timers.bru │ │ │ │ │ ├── url.bru │ │ │ │ │ └── web-crypto.bru │ │ │ │ └── npm modules/ │ │ │ │ ├── ajv.bru │ │ │ │ ├── external-lib-bru-req-res.bru │ │ │ │ ├── fakerjs.bru │ │ │ │ └── jose.bru │ │ │ ├── string interpolation/ │ │ │ │ ├── env vars.bru │ │ │ │ ├── folder.bru │ │ │ │ ├── missing values.bru │ │ │ │ ├── objects-arrays interpolation.bru │ │ │ │ ├── process env vars.bru │ │ │ │ └── runtime vars.bru │ │ │ └── url-serialization/ │ │ │ ├── Duplicate Keys.bru │ │ │ └── folder.bru │ │ ├── collection_level_oauth2/ │ │ │ ├── .gitignore │ │ │ ├── .nvmrc │ │ │ ├── bruno.json │ │ │ ├── collection.bru │ │ │ ├── environments/ │ │ │ │ ├── Local.bru │ │ │ │ └── Prod.bru │ │ │ ├── package.json │ │ │ ├── readme.md │ │ │ └── resource.bru │ │ ├── collection_oauth2/ │ │ │ ├── .env │ │ │ ├── .gitignore │ │ │ ├── .nvmrc │ │ │ ├── auth/ │ │ │ │ └── oauth2/ │ │ │ │ ├── authorization_code/ │ │ │ │ │ ├── github token with authorize.bru │ │ │ │ │ ├── google token with authorize.bru │ │ │ │ │ ├── resource.bru │ │ │ │ │ └── token with authorize.bru │ │ │ │ ├── client_credentials/ │ │ │ │ │ ├── resource.bru │ │ │ │ │ └── token.bru │ │ │ │ └── password_credentials/ │ │ │ │ ├── resource.bru │ │ │ │ └── token.bru │ │ │ ├── bruno.json │ │ │ ├── collection.bru │ │ │ ├── environments/ │ │ │ │ ├── Local.bru │ │ │ │ └── Prod.bru │ │ │ ├── file.json │ │ │ ├── package.json │ │ │ └── readme.md │ │ ├── external-lib-with-bru-req-res-objects/ │ │ │ ├── index.js │ │ │ └── package.json │ │ ├── keycloak-authorization_code/ │ │ │ ├── bruno.json │ │ │ ├── collection.bru │ │ │ ├── environments/ │ │ │ │ └── oauth2.bru │ │ │ ├── user_info_coll-auth.bru │ │ │ ├── user_info_custom.bru │ │ │ └── user_info_request-auth.bru │ │ ├── keycloak-client-credentials/ │ │ │ ├── bruno.json │ │ │ ├── collection.bru │ │ │ ├── environments/ │ │ │ │ └── oauth2.bru │ │ │ ├── user_info_coll-auth.bru │ │ │ ├── user_info_custom.bru │ │ │ └── user_info_request-auth.bru │ │ ├── keycloak-password-credentials/ │ │ │ ├── bruno.json │ │ │ ├── collection.bru │ │ │ ├── environments/ │ │ │ │ └── oauth2.bru │ │ │ ├── user_info_coll-auth.bru │ │ │ ├── user_info_custom.bru │ │ │ └── user_info_request-auth.bru │ │ ├── package.json │ │ ├── readme.md │ │ ├── sandwich_exec/ │ │ │ ├── bruno.json │ │ │ ├── collection.bru │ │ │ └── folder/ │ │ │ ├── folder.bru │ │ │ └── request.bru │ │ ├── sequential_exec/ │ │ │ ├── bruno.json │ │ │ ├── collection.bru │ │ │ └── folder/ │ │ │ ├── folder.bru │ │ │ └── request.bru │ │ └── src/ │ │ ├── auth/ │ │ │ ├── basic.js │ │ │ ├── bearer.js │ │ │ ├── cookie.js │ │ │ ├── index.js │ │ │ ├── oauth2/ │ │ │ │ ├── authorizationCode.js │ │ │ │ ├── clientCredentials.js │ │ │ │ └── passwordCredentials.js │ │ │ └── wsse.js │ │ ├── echo/ │ │ │ └── index.js │ │ ├── graphql/ │ │ │ └── index.js │ │ ├── index.js │ │ ├── mix/ │ │ │ └── index.js │ │ ├── multipart/ │ │ │ ├── form-data-parser.js │ │ │ └── index.js │ │ ├── redirect/ │ │ │ └── index.js │ │ ├── utils/ │ │ │ └── xmlParser.js │ │ └── ws/ │ │ └── index.js │ └── bruno-toml/ │ ├── lib/ │ │ └── stringify │ ├── package.json │ ├── src/ │ │ ├── jsonToToml.js │ │ └── tomlToJson.js │ └── tests/ │ ├── headers/ │ │ ├── disabled-header/ │ │ │ ├── request.json │ │ │ └── request.toml │ │ ├── dotted-header/ │ │ │ ├── request.json │ │ │ └── request.toml │ │ ├── duplicate-header/ │ │ │ ├── request.json │ │ │ └── request.toml │ │ ├── empty-header/ │ │ │ ├── request.json │ │ │ └── request.toml │ │ ├── reserved-header/ │ │ │ ├── request.json │ │ │ └── request.toml │ │ ├── simple-header/ │ │ │ ├── request.json │ │ │ └── request.toml │ │ ├── spaces-in-header/ │ │ │ ├── request.json │ │ │ └── request.toml │ │ └── unicode-in-header/ │ │ ├── request.json │ │ └── request.toml │ ├── index.spec.js │ ├── methods/ │ │ ├── delete/ │ │ │ ├── request.json │ │ │ └── request.toml │ │ └── get/ │ │ ├── request.json │ │ └── request.toml │ └── scripts/ │ ├── post-response/ │ │ ├── request.json │ │ └── request.toml │ ├── pre-request/ │ │ ├── request.json │ │ └── request.toml │ └── tests/ │ ├── request.json │ └── request.toml ├── playwright/ │ ├── codegen.ts │ ├── electron.ts │ └── index.ts ├── playwright.config.ts ├── publishing.md ├── readme.md ├── scripts/ │ ├── build-electron.js │ ├── build-electron.sh │ ├── changed-packages.js │ ├── count-locs.js │ ├── dev-hot-reload.js │ ├── dev.js │ ├── pr-checkout.js │ └── setup.js ├── security.md └── tests/ ├── asserts/ │ ├── add-assertions.spec.ts │ ├── fixtures/ │ │ └── collection/ │ │ ├── bruno.json │ │ ├── environments/ │ │ │ └── Local.bru │ │ └── ping.bru │ └── init-user-data/ │ └── preferences.json ├── collection/ │ ├── close-all-collections/ │ │ ├── close-all-collections.spec.ts │ │ ├── fixtures/ │ │ │ └── collections/ │ │ │ ├── collection 1/ │ │ │ │ ├── bruno.json │ │ │ │ └── test-request.bru │ │ │ └── collection 2/ │ │ │ ├── bruno.json │ │ │ └── test-request.bru │ │ └── init-user-data/ │ │ └── preferences.json │ ├── create/ │ │ └── create-collection.spec.ts │ ├── create-requests/ │ │ ├── fixtures/ │ │ │ └── collection/ │ │ │ ├── bruno.json │ │ │ └── folder1/ │ │ │ └── folder.bru │ │ ├── graphql-requests.spec.ts │ │ ├── grpc-requests.spec.ts │ │ ├── http-requests.spec.ts │ │ ├── init-user-data/ │ │ │ ├── collection-security.json │ │ │ └── preferences.json │ │ └── ws-requests.spec.ts │ ├── default-ignores/ │ │ └── default-ignores.spec.ts │ ├── default-sandbox-mode/ │ │ └── default-sandbox-mode.spec.ts │ ├── delete/ │ │ └── delete-collection.spec.ts │ ├── draft/ │ │ ├── draft-indicator.spec.ts │ │ ├── draft-values-in-requests.spec.ts │ │ └── fixtures/ │ │ ├── grpcbin.proto │ │ └── mitmproxy-ca-cert.cer │ ├── moving-requests/ │ │ ├── cross-collection-drag-drop-folder.spec.ts │ │ ├── cross-collection-drag-drop-request.spec.ts │ │ └── tag-persistence.spec.ts │ ├── moving-tabs/ │ │ └── move-tabs.spec.ts │ └── open/ │ └── open-multiple-collections.spec.ts ├── cookies/ │ ├── cookie-persistence.spec.ts │ └── corrupted-passkey.spec.ts ├── devtools/ │ └── performance/ │ └── performance-tab.spec.ts ├── dotenv/ │ └── special-chars-collection-path/ │ └── dotenv-special-chars.spec.ts ├── editable-table/ │ └── editable-table.spec.ts ├── environments/ │ ├── api-deleteEnvVar/ │ │ ├── api-deleteEnvVar.spec.ts │ │ ├── fixtures/ │ │ │ └── collection/ │ │ │ ├── api-deleteEnvVar.bru │ │ │ ├── bruno.json │ │ │ └── environments/ │ │ │ └── Stage.bru │ │ └── init-user-data/ │ │ ├── collection-security.json │ │ └── preferences.json │ ├── api-setEnvVar/ │ │ ├── api-setEnvVar-with-persist.spec.ts │ │ ├── api-setEnvVar-without-persist.spec.ts │ │ ├── fixtures/ │ │ │ └── collection/ │ │ │ ├── api-setEnvVar-with-persist.bru │ │ │ ├── api-setEnvVar-without-persist.bru │ │ │ ├── bruno.json │ │ │ ├── environments/ │ │ │ │ └── Stage.bru │ │ │ └── multiple-persist-vars-folder/ │ │ │ ├── folder.bru │ │ │ ├── multiple-persist-vars-1.bru │ │ │ └── multiple-persist-vars-2.bru │ │ ├── init-user-data/ │ │ │ ├── collection-security.json │ │ │ └── preferences.json │ │ └── multiple-persist-vars.spec.ts │ ├── collection-env-config-selection/ │ │ ├── collection/ │ │ │ ├── bruno.json │ │ │ ├── environments/ │ │ │ │ ├── dev.bru │ │ │ │ └── prod.bru │ │ │ └── test-request.bru │ │ ├── collection-env-config-selection.spec.ts │ │ └── init-user-data/ │ │ ├── collection-security.json │ │ └── preferences.json │ ├── color-picker/ │ │ ├── collection/ │ │ │ ├── bruno.json │ │ │ └── test-request.bru │ │ ├── color-picker.spec.ts │ │ └── init-user-data/ │ │ ├── collection-security.json │ │ ├── global-environments.json │ │ └── preferences.json │ ├── create-environment/ │ │ ├── collection-env-create.spec.ts │ │ ├── fixtures/ │ │ │ └── bruno-collection.json │ │ └── global-env-create.spec.ts │ ├── export-environment/ │ │ ├── collection-env-export/ │ │ │ ├── collection-env-export.spec.ts │ │ │ ├── fixtures/ │ │ │ │ └── collection/ │ │ │ │ ├── bruno.json │ │ │ │ ├── environments/ │ │ │ │ │ ├── local.bru │ │ │ │ │ └── prod.bru │ │ │ │ └── test-request.bru │ │ │ └── init-user-data/ │ │ │ ├── collection-security.json │ │ │ ├── global-environments.json │ │ │ └── preferences.json │ │ └── global-env-export/ │ │ ├── fixtures/ │ │ │ └── collection/ │ │ │ ├── bruno.json │ │ │ └── test-request.bru │ │ ├── global-env-export.spec.ts │ │ └── init-user-data/ │ │ ├── collection-security.json │ │ ├── global-environments.json │ │ └── preferences.json │ ├── fixtures/ │ │ └── environment-exports/ │ │ ├── bruno-collection-environments/ │ │ │ ├── local.json │ │ │ └── prod.json │ │ ├── bruno-collection-environments.json │ │ ├── bruno-global-environments/ │ │ │ ├── local.json │ │ │ └── prod.json │ │ ├── bruno-global-environments.json │ │ └── local.json │ ├── focus-retention/ │ │ └── environment-focus.spec.ts │ ├── global-env-config-selection/ │ │ ├── collection/ │ │ │ ├── bruno.json │ │ │ └── test-request.bru │ │ ├── global-env-config-selection.spec.ts │ │ └── init-user-data/ │ │ ├── collection-security.json │ │ ├── global-environments.json │ │ └── preferences.json │ ├── import-environment/ │ │ ├── bruno-env-import/ │ │ │ ├── collection-env-import/ │ │ │ │ ├── collection-env-import.spec.ts │ │ │ │ ├── fixtures/ │ │ │ │ │ └── collection/ │ │ │ │ │ ├── bruno.json │ │ │ │ │ └── test-request.bru │ │ │ │ └── init-user-data/ │ │ │ │ ├── collection-security.json │ │ │ │ └── preferences.json │ │ │ └── global-env-import/ │ │ │ ├── fixtures/ │ │ │ │ └── collection/ │ │ │ │ ├── bruno.json │ │ │ │ └── test-request.bru │ │ │ ├── global-env-import.spec.ts │ │ │ └── init-user-data/ │ │ │ ├── collection-security.json │ │ │ └── preferences.json │ │ ├── collection-env-import.spec.ts │ │ ├── env-color-import/ │ │ │ ├── env-color-import.spec.ts │ │ │ ├── fixtures/ │ │ │ │ ├── collection/ │ │ │ │ │ ├── bruno.json │ │ │ │ │ └── test-request.bru │ │ │ │ ├── env-with-color.json │ │ │ │ └── multiple-envs-with-colors.json │ │ │ └── init-user-data/ │ │ │ ├── collection-security.json │ │ │ └── preferences.json │ │ ├── fixtures/ │ │ │ ├── collection-env.json │ │ │ ├── collection.json │ │ │ └── global-env.json │ │ └── global-env-import.spec.ts │ ├── multiline-variables/ │ │ ├── fixtures/ │ │ │ └── collection/ │ │ │ ├── bruno.json │ │ │ ├── collection.bru │ │ │ ├── environments/ │ │ │ │ └── Test.bru │ │ │ ├── multiline-test.bru │ │ │ └── request.bru │ │ ├── init-user-data/ │ │ │ ├── collection-security.json │ │ │ └── preferences.json │ │ ├── read-multiline-environment.spec.ts │ │ └── write-multiline-variable.spec.ts │ └── update-global-environment-via-script/ │ ├── fixtures/ │ │ └── collection/ │ │ ├── bruno.json │ │ ├── collection.bru │ │ └── test-request.bru │ ├── global-env-update-via-script.spec.ts │ └── init-user-data/ │ ├── collection-security.json │ ├── global-environments.json │ └── preferences.json ├── footer/ │ ├── notifications/ │ │ └── notifications.spec.js │ └── sidebar-toggle/ │ └── sidebar-toggle.spec.js ├── global-environments/ │ ├── collection/ │ │ ├── bruno.json │ │ └── set-global-nonstring.bru │ ├── init-user-data/ │ │ └── preferences.json │ └── non-string-values.spec.ts ├── grpc/ │ ├── make-request/ │ │ ├── fixtures/ │ │ │ └── collection/ │ │ │ ├── HelloService/ │ │ │ │ ├── BidiHello.bru │ │ │ │ ├── LotOfGreetings.bru │ │ │ │ ├── LotOfReplies.bru │ │ │ │ ├── SayHello.bru │ │ │ │ └── folder.bru │ │ │ ├── bruno.json │ │ │ ├── collection.bru │ │ │ └── environments/ │ │ │ └── Env.bru │ │ ├── init-user-data/ │ │ │ ├── collection-security.json │ │ │ └── preferences.json │ │ └── make-request.spec.ts │ ├── metadata/ │ │ ├── fixtures/ │ │ │ └── collection/ │ │ │ ├── bruno.json │ │ │ ├── collection.bru │ │ │ └── sayHello.bru │ │ ├── init-user-data/ │ │ │ ├── collection-security.json │ │ │ └── preferences.json │ │ └── with-bin-metadata.spec.ts │ └── method-search/ │ ├── fixtures/ │ │ └── grpc-collection/ │ │ ├── SayHello.bru │ │ ├── bruno.json │ │ └── environments/ │ │ └── GrpcEnv.bru │ ├── grpc-method-search.spec.ts │ └── init-user-data/ │ ├── collection-security.json │ └── preferences.json ├── import/ │ ├── bruno/ │ │ ├── fixtures/ │ │ │ ├── bruno-invalid-corrupted.json │ │ │ ├── bruno-malformed.json │ │ │ ├── bruno-missing-required-fields.json │ │ │ ├── bruno-testbench.json │ │ │ └── bruno-with-examples.json │ │ ├── import-bruno-corrupted-fails.spec.ts │ │ ├── import-bruno-missing-required-schema.spec.ts │ │ ├── import-bruno-testbench.spec.ts │ │ └── import-bruno-with-examples.spec.ts │ ├── bulk-import/ │ │ ├── 001-multiple-files-upload.spec.ts │ │ └── 002-all-collection-types.spec.ts │ ├── file-types/ │ │ ├── file-input-acceptance.spec.ts │ │ ├── fixtures/ │ │ │ └── invalid.txt │ │ └── invalid-file-handling.spec.ts │ ├── insomnia/ │ │ ├── fixtures/ │ │ │ ├── insomnia-malformed.json │ │ │ ├── insomnia-v4-with-envs.json │ │ │ ├── insomnia-v4.json │ │ │ ├── insomnia-v5-invalid-missing-collection.yaml │ │ │ ├── insomnia-v5-with-envs.yaml │ │ │ └── insomnia-v5.yaml │ │ ├── import-insomnia-v4-environments.spec.ts │ │ ├── import-insomnia-v4.spec.ts │ │ ├── import-insomnia-v5-environments.spec.ts │ │ ├── import-insomnia-v5.spec.ts │ │ ├── invalid-missing-collection.spec.ts │ │ └── malformed-structure.spec.ts │ ├── openapi/ │ │ ├── cli/ │ │ │ ├── fixtures/ │ │ │ │ └── openapi.json │ │ │ └── group-by-import.spec.ts │ │ ├── duplicate-operation-names-fix.spec.ts │ │ ├── fixtures/ │ │ │ ├── openapi-comprehensive.yaml │ │ │ ├── openapi-duplicate-operation-name.yaml │ │ │ ├── openapi-invalid-version.yaml │ │ │ ├── openapi-malformed.yaml │ │ │ ├── openapi-missing-info.yaml │ │ │ ├── openapi-newline-in-operation-name.yaml │ │ │ ├── openapi-path-grouping.json │ │ │ ├── openapi-simple.json │ │ │ ├── openapi-with-examples.yaml │ │ │ ├── openapi-with-security-schemes.json │ │ │ └── openapi-without-security-schemes.json │ │ ├── import-openapi-json.spec.ts │ │ ├── import-openapi-with-examples.spec.ts │ │ ├── import-openapi-yaml.spec.ts │ │ ├── malformed-yaml.spec.ts │ │ ├── missing-info.spec.ts │ │ ├── operation-name-with-newlines-fix.spec.ts │ │ ├── path-based-grouping.spec.ts │ │ └── security-schemes-import.spec.ts │ ├── postman/ │ │ ├── fixtures/ │ │ │ ├── postman-invalid-missing-info.json │ │ │ ├── postman-invalid-schema.json │ │ │ ├── postman-malformed.json │ │ │ ├── postman-v20.json │ │ │ ├── postman-v21.json │ │ │ └── postman-with-examples.json │ │ ├── import-postman-v20.spec.ts │ │ ├── import-postman-v21.spec.ts │ │ ├── import-postman-with-examples.spec.ts │ │ ├── invalid-json.spec.ts │ │ ├── invalid-missing-info.spec.ts │ │ ├── invalid-schema.spec.ts │ │ └── malformed-structure.spec.ts │ ├── test-data/ │ │ ├── sample-bruno.json │ │ ├── sample-insomnia.json │ │ ├── sample-openapi.yaml │ │ └── sample-postman.json │ ├── url-import/ │ │ ├── github-repository-import.spec.ts │ │ ├── insomnia-url-import.spec.ts │ │ ├── openapi-url-import.spec.ts │ │ └── postman-url-import.spec.ts │ └── wsdl/ │ ├── fixtures/ │ │ ├── wsdl-bruno.json │ │ └── wsdl.xml │ └── import-wsdl.spec.ts ├── interpolation/ │ ├── collection/ │ │ ├── bruno.json │ │ ├── echo-request-odata.bru │ │ └── echo-request-url.bru │ ├── dynamic-variable/ │ │ ├── collection/ │ │ │ ├── bruno.json │ │ │ └── set-var-dynamic-variable.bru │ │ ├── init-user-data/ │ │ │ └── preferences.json │ │ └── set-var-dynamic-variable.spec.ts │ ├── init-user-data/ │ │ ├── collection-security.json │ │ └── preferences.json │ ├── interpolate-request-url.spec.ts │ └── prompt-variables/ │ ├── fixtures/ │ │ ├── client.pfx │ │ └── collection/ │ │ ├── bruno.json │ │ ├── collection.bru │ │ ├── environments/ │ │ │ └── local.bru │ │ └── http-folder/ │ │ ├── folder.bru │ │ ├── http-request-without-ca.bru │ │ └── https-request-with-ca.bru │ ├── http-request-prompt-variables.spec.ts │ └── init-user-data/ │ ├── collection-security.json │ ├── global-environments.json │ ├── preferences.json │ └── ui-state-snapshot.json ├── onboarding/ │ ├── init-user-data/ │ │ └── preferences.json │ ├── init-user-data-fresh/ │ │ └── preferences.json │ ├── sample-collection.spec.ts │ └── welcome-modal.spec.ts ├── preferences/ │ ├── autosave/ │ │ └── autosave.spec.ts │ ├── default-collection-location/ │ │ ├── collection/ │ │ │ ├── bruno.json │ │ │ ├── collection.bru │ │ │ ├── environments/ │ │ │ │ └── Test.bru │ │ │ └── request.bru │ │ ├── default-collection-location.spec.js │ │ └── init-user-data/ │ │ ├── collection-security.json │ │ └── preferences.json │ ├── support-links.spec.js │ └── tab-switch-persistence/ │ └── tab-switch-persistence.spec.ts ├── protobuf/ │ ├── fixtures/ │ │ └── collection/ │ │ ├── HelloService/ │ │ │ ├── folder.bru │ │ │ └── sayHello.bru │ │ ├── bruno.json │ │ ├── collection.bru │ │ ├── environments/ │ │ │ └── GrpcEnv.bru │ │ └── protos/ │ │ ├── services/ │ │ │ ├── order.proto │ │ │ └── product.proto │ │ └── types/ │ │ └── product-message.proto │ ├── init-user-data/ │ │ ├── collection-security.json │ │ └── preferences.json │ └── manage-protofile.spec.ts ├── request/ │ ├── body-scroll/ │ │ └── body-scroll-restoration.spec.ts │ ├── collections/ │ │ └── custom-search/ │ │ ├── bruno.json │ │ ├── package.json │ │ └── search-request.bru │ ├── copy-request/ │ │ ├── copy-folder.spec.ts │ │ ├── copy-request.spec.ts │ │ └── keyboard-shortcuts.spec.ts │ ├── delete-request/ │ │ └── delete-request-sequence-updation.spec.ts │ ├── encoding/ │ │ ├── collection/ │ │ │ ├── bruno.json │ │ │ ├── encode-url-preencoded.bru │ │ │ ├── encode-url-unencoded.bru │ │ │ ├── raw-url-preencoded.bru │ │ │ └── raw-url-unencoded.bru │ │ ├── curl-encoding.spec.ts │ │ └── init-user-data/ │ │ ├── collection-security.json │ │ └── preferences.json │ ├── headers/ │ │ └── header-validation.spec.ts │ ├── newlines/ │ │ └── newlines-persistence.spec.ts │ ├── response-pane-update-when-focused.spec.ts │ ├── save/ │ │ └── save.spec.ts │ ├── settings/ │ │ ├── collection/ │ │ │ ├── bruno.json │ │ │ ├── max-redirects.bru │ │ │ ├── no-redirects.bru │ │ │ └── timeout.bru │ │ ├── init-user-data/ │ │ │ ├── collection-security.json │ │ │ └── preferences.json │ │ ├── max-redirects.spec.ts │ │ ├── no-redirects.spec.ts │ │ └── timeout.spec.ts │ └── tests/ │ └── custom-search/ │ ├── custom-search.spec.ts │ └── init-user-data/ │ ├── collection-security.json │ └── preferences.json ├── response/ │ ├── json-response-formatting/ │ │ ├── fixtures/ │ │ │ └── collection/ │ │ │ ├── bruno.json │ │ │ └── request.bru │ │ ├── init-user-data/ │ │ │ ├── collection-security.json │ │ │ └── preferences.json │ │ └── json-response-formatting.spec.ts │ ├── large-response-crash-prevention.spec.ts │ ├── response-actions.spec.ts │ └── response-format-select-and-preview/ │ ├── fixtures/ │ │ └── collection/ │ │ ├── bruno.json │ │ ├── request-html.bru │ │ └── request-json.bru │ ├── init-user-data/ │ │ ├── collection-security.json │ │ └── preferences.json │ └── response-format-select-and-preview.spec.ts ├── response-examples/ │ ├── create-example.spec.ts │ ├── edit-example.spec.ts │ ├── fixtures/ │ │ └── collection/ │ │ ├── bruno.json │ │ ├── create-example.bru │ │ ├── edit-example.bru │ │ └── menu-operations.bru │ ├── init-user-data/ │ │ ├── collection-security.json │ │ └── preferences.json │ └── menu-operations.spec.ts ├── runner/ │ ├── cli-env-combined/ │ │ ├── cli-env-combined.spec.ts │ │ └── collection/ │ │ ├── bruno.json │ │ ├── environments/ │ │ │ └── CollectionEnv.bru │ │ ├── global-env.json │ │ └── request.bru │ ├── cli-json-env-file/ │ │ ├── cli-json-env-file.spec.ts │ │ └── collection/ │ │ ├── bruno.json │ │ ├── env.json │ │ └── request.bru │ ├── collection-run-report/ │ │ ├── collection/ │ │ │ ├── api/ │ │ │ │ └── v1/ │ │ │ │ ├── posts.bru │ │ │ │ └── users.bru │ │ │ ├── auth/ │ │ │ │ ├── login.bru │ │ │ │ └── logout.bru │ │ │ └── bruno.json │ │ ├── collection-run-report.spec.ts │ │ └── collection-run-report.spec.ts-snapshots/ │ │ ├── cli-junit-report-default-darwin.xml │ │ └── cli-junit-report-default-linux.xml │ ├── collection-run.ts │ └── init-user-data/ │ └── preferences.json ├── scratch-requests/ │ └── scratch-requests.spec.ts ├── scripting/ │ ├── bru-api/ │ │ └── isSafeMode/ │ │ ├── fixtures/ │ │ │ └── collections/ │ │ │ └── is-safe-mode-test/ │ │ │ ├── bruno.json │ │ │ ├── test-safe-mode-false.bru │ │ │ └── test-safe-mode-true.bru │ │ ├── init-user-data/ │ │ │ ├── collection-security.json │ │ │ └── preferences.json │ │ └── isSafeMode.spec.ts │ ├── inbuilt-libraries/ │ │ ├── fs/ │ │ │ ├── fixtures/ │ │ │ │ └── collections/ │ │ │ │ └── should_allow_fs/ │ │ │ │ ├── bruno.json │ │ │ │ └── request.bru │ │ │ ├── fs.spec.ts │ │ │ └── init-user-data/ │ │ │ └── preferences.json │ │ └── jsonwebtoken/ │ │ ├── fixtures/ │ │ │ └── collection/ │ │ │ ├── bruno.json │ │ │ ├── decode/ │ │ │ │ ├── decode.bru │ │ │ │ └── folder.bru │ │ │ ├── environments/ │ │ │ │ └── Prod.bru │ │ │ ├── sign/ │ │ │ │ ├── folder.bru │ │ │ │ ├── sign with callback err.bru │ │ │ │ ├── sign with callback token.bru │ │ │ │ └── sign.bru │ │ │ └── verify/ │ │ │ ├── folder.bru │ │ │ ├── verify with callback err.bru │ │ │ ├── verify with callback token.bru │ │ │ └── verify.bru │ │ ├── init-user-data/ │ │ │ ├── preferences.json │ │ │ └── ui-state-snapshot.json │ │ └── jsonwebtoken.spec.ts │ └── url-helpers/ │ ├── fixtures/ │ │ └── collections/ │ │ └── url_helpers_test/ │ │ ├── bruno.json │ │ └── url-helpers-test.bru │ ├── init-user-data/ │ │ └── preferences.json │ └── url-helpers.spec.ts ├── sidebar/ │ ├── rename-collection-item.spec.ts │ └── section-auto-expand.spec.ts ├── ssl/ │ ├── basic-ssl/ │ │ ├── collections/ │ │ │ ├── badssl/ │ │ │ │ ├── bruno.json │ │ │ │ ├── package.json │ │ │ │ └── request.bru │ │ │ └── self-signed-badssl/ │ │ │ ├── bruno.json │ │ │ ├── package.json │ │ │ └── request.bru │ │ └── tests/ │ │ ├── basic-ssl-success/ │ │ │ ├── basic-ssl-success.spec.ts │ │ │ └── init-user-data/ │ │ │ └── preferences.json │ │ ├── self-signed-rejected/ │ │ │ ├── init-user-data/ │ │ │ │ └── preferences.json │ │ │ └── self-signed-rejected.spec.ts │ │ └── self-signed-success-with-validation-disabled/ │ │ ├── init-user-data/ │ │ │ └── preferences.json │ │ └── self-signed-success-with-validation-disabled.spec.ts │ └── custom-ca-certs/ │ ├── collection/ │ │ ├── bruno.json │ │ ├── package.json │ │ └── request.bru │ ├── server/ │ │ ├── .gitignore │ │ ├── helpers/ │ │ │ ├── certs.js │ │ │ └── platform.js │ │ ├── index.js │ │ ├── readme.md │ │ └── scripts/ │ │ └── generate-certs.js │ └── tests/ │ ├── custom-invalid-ca-cert-in-config/ │ │ ├── custom-invalid-ca-cert-in-config.spec.ts │ │ └── init-user-data/ │ │ └── preferences.json │ ├── custom-invalid-ca-cert-in-config-with-defaults/ │ │ ├── custom-invalid-ca-cert-in-config-with-defaults.spec.ts │ │ └── init-user-data/ │ │ └── preferences.json │ ├── custom-valid-ca-cert-in-config/ │ │ ├── custom-valid-ca-cert-in-config.spec.ts │ │ └── init-user-data/ │ │ └── preferences.json │ ├── custom-valid-ca-cert-in-config-with-defaults/ │ │ ├── custom-valid-ca-cert-in-config-with-defaults.spec.ts │ │ └── init-user-data/ │ │ └── preferences.json │ └── wss-success/ │ ├── fixtures/ │ │ └── wss-collection/ │ │ ├── bruno.json │ │ ├── package.json │ │ └── ws-ssl-request.bru │ ├── init-user-data/ │ │ └── preferences.json │ └── wss-success.spec.ts ├── start/ │ └── app-open.spec.ts ├── transient-requests/ │ └── transient-requests.spec.ts ├── utils/ │ ├── page/ │ │ ├── actions.ts │ │ ├── index.ts │ │ ├── locators.ts │ │ ├── navigation.ts │ │ └── runner.ts │ └── wait.ts ├── variable-tooltip/ │ └── variable-tooltip.spec.ts ├── websockets/ │ ├── connection.spec.ts │ ├── fixtures/ │ │ └── collection/ │ │ ├── base.bru │ │ ├── bruno.json │ │ ├── collection.bru │ │ ├── ws-test-request-with-headers.bru │ │ ├── ws-test-request-with-query.bru │ │ ├── ws-test-request-with-subproto.bru │ │ └── ws-test-request.bru │ ├── headers.spec.ts │ ├── init-user-data/ │ │ ├── collection-security.json │ │ └── preferences.json │ ├── persistence.spec.ts │ ├── query.spec.ts │ ├── subproto.spec.ts │ └── variable-interpolation/ │ ├── fixtures/ │ │ └── collection/ │ │ ├── bruno.json │ │ ├── environments/ │ │ │ └── Test.bru │ │ └── ws-interpolation-test.bru │ ├── init-user-data/ │ │ └── preferences.json │ └── variable-interpolation.spec.ts └── workspace/ ├── close-tab-stays-in-workspace.spec.ts ├── collection-reorder-persistence.spec.ts ├── create-workspace/ │ ├── create-workspace.spec.ts │ └── init-user-data/ │ └── preferences.json └── default-workspace/ ├── default-workspace.spec.ts ├── migration.spec.ts └── recovery-and-backup.spec.ts ================================================ FILE CONTENTS ================================================ ================================================ FILE: .coderabbit.yaml ================================================ # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json language: 'en-US' early_access: false tone_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.' knowledge_base: opt_out: false code_guidelines: enabled: true filePatterns: - '**/CODING_STANDARDS.md' reviews: profile: 'chill' request_changes_workflow: false high_level_summary: true poem: true review_status: true collapse_walkthrough: false auto_review: enabled: true drafts: false base_branches: ['main', 'release/*'] path_instructions: - path: '**/*' instructions: | Bruno is a cross-platform Electron desktop app that runs on macOS, Windows, and Linux. Ensure that all code is OS-agnostic: - File paths must use `path.join()` or `path.resolve()` instead of hardcoded `/` or `\\` separators - Never assume case-sensitive or case-insensitive filesystems - Use `os.homedir()`, `app.getPath()`, or environment-appropriate APIs instead of hardcoded paths like `/home/`, `C:\\Users\\`, or `~/` - Line endings should be handled consistently (be aware of CRLF vs LF issues) - Use `path.sep` or `path.posix`/`path.win32` when platform-specific separators are needed - Shell commands or child_process calls must account for platform differences (e.g., `which` vs `where`, `/bin/sh` vs `cmd.exe`) - File permissions (e.g., `fs.chmod`, `fs.access`) should account for Windows not supporting Unix-style permission bits - Avoid relying on Unix-only signals (e.g., `SIGKILL`) without Windows fallbacks - Use `os.tmpdir()` instead of hardcoding `/tmp` - Environment variable access should handle platform differences (e.g., `HOME` vs `USERPROFILE`) - path: 'tests/**/**.*' instructions: | Review the following e2e test code written using the Playwright test library. Ensure that: - Follow best practices for Playwright code and e2e automation - Try to reduce usage of `page.waitForTimeout();` in code unless absolutely necessary and the locator cannot be found using existing `expect()` playwright calls - Avoid using `page.pause()` in code - Use locator variables for locators - Avoid using test.only - Use multiple assertions - Promote the use of `test.step` as much as possible so the generated reports are easier to read - Ensure that the `fixtures` like the collections are nested inside the `fixtures` folder **Fixture Example***: Here's an example of possible fixture and test pair ``` . ├── fixtures │ └── collection │ ├── base.bru │ ├── bruno.json │ ├── collection.bru │ ├── ws-test-request-with-headers.bru │ ├── ws-test-request-with-subproto.bru │ └── ws-test-request.bru ├── connection.spec.ts # <- Depends on the collection in ./fixtures/collection ├── headers.spec.ts ├── persistence.spec.ts ├── variable-interpolation │ ├── fixtures │ │ └── collection │ │ ├── environments │ │ ├── bruno.json │ │ └── ws-interpolation-test.bru │ ├── init-user-data │ └── variable-interpolation.spec.ts # <- Depends on the collection in ./variable-interpolation/fixtures/collection └── subproto.spec.ts ``` chat: auto_reply: true ================================================ FILE: .github/CODEOWNERS ================================================ * @helloanoop @maintainer-bruno @bijin-bruno @lohit-bruno @naman-bruno ================================================ FILE: .github/ISSUE_TEMPLATE/BugReport.yaml ================================================ name: Bug Report description: File a bug report labels: ['bug'] body: - type: markdown attributes: value: | Thanks for taking the time to fill out this bug report! Before submitting, please make sure you've searched existing issues: 👉 [Search existing issues](https://github.com/usebruno/bruno/issues?q=is%3Aissue) - type: checkboxes attributes: label: 'I have checked the following:' options: - label: "I have searched existing issues and found nothing related to my issue." required: true - type: checkboxes attributes: label: 'This bug is:' options: - label: making Bruno unusable for me required: false - label: slowing me down but I'm able to continue working required: false - label: annoying required: false - label: this feature was working in a previous version but is broken in the current release. required: false - type: input attributes: label: Bruno version description: Please specify the version of Bruno you are using in which the issue occurs. placeholder: 1.38.1 validations: required: true - type: input attributes: label: Operating System description: Information about the operating system the issue occurs on. placeholder: Windows 11 26100.3037 / macOS 15.1 (24B83) / Linux 6.13.1 validations: required: true - type: textarea attributes: label: Describe the bug description: A clear and concise description of the bug and how it's effecting your work along with steps to reproduce. validations: required: true - type: textarea attributes: label: .bru file to reproduce the bug description: Attach your .bru file here that can reproduce the problem. validations: required: false - type: textarea attributes: label: Screenshots/Live demo link description: Add some screenshots to help explain the problem. validations: required: true ================================================ FILE: .github/ISSUE_TEMPLATE/FeatureRequest.yaml ================================================ name: Feature Request description: Suggest an idea for this project. labels: ['enhancement'] body: - type: checkboxes attributes: label: 'I have checked the following:' options: - label: I've searched existing issues and found nothing related to my issue. required: true - type: checkboxes attributes: label: 'This feature' options: - label: blocks me from using Bruno required: false - label: would improve my quality of life in Bruno required: false - label: is something I've never seen an API client do before required: false - type: markdown attributes: value: | Suggest an idea for this project. - type: textarea attributes: label: Describe the feature you want to add, and how it would change your usage of Bruno description: A clear and concise description of the feature you want to be added. validations: required: true - type: textarea attributes: label: Mockups or Images of the feature description: Add some images to support your feature. validations: required: false ================================================ FILE: .github/ISSUE_TEMPLATE/config.yaml ================================================ blank_issues_enabled: true contact_links: - name: Discussions url: https://github.com/usebruno/bruno/discussions about: You can ask general questions or give feedback here. - name: Discord Server url: https://discord.com/invite/KgcZUncpjq about: Join our Discord community to chat about Bruno. ================================================ FILE: .github/PULL_REQUEST_TEMPLATE.md ================================================ ### Description #### Contribution Checklist: - [ ] **I've used AI significantly to create this pull request** - [ ] **The pull request only addresses one issue or adds one feature.** - [ ] **The pull request does not introduce any breaking changes** - [ ] **I have added screenshots or gifs to help explain the change if applicable.** - [ ] **I have read the [contribution guidelines](https://github.com/usebruno/bruno/blob/main/contributing.md).** - [ ] **Create an issue and link to the pull request.** Note: 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. #### Publishing to New Package Managers Please see [here](../publishing.md) for more information. ================================================ FILE: .github/actions/common/setup-node-deps/action.yml ================================================ name: 'Setup Node Dependencies' description: 'Install Node.js and npm dependencies' inputs: skip-build: description: 'Skip building libraries' required: false default: 'false' runs: using: 'composite' steps: - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: v22.17.0 cache: 'npm' cache-dependency-path: './package-lock.json' - name: Install node dependencies shell: bash run: npm ci --legacy-peer-deps - name: Build libraries if: inputs.skip-build != 'true' shell: bash run: | npm run build:graphql-docs npm run build:bruno-query npm run build:bruno-common npm run sandbox:bundle-libraries --workspace=packages/bruno-js npm run build:bruno-converters npm run build:bruno-requests npm run build:schema-types npm run build:bruno-filestore ================================================ FILE: .github/actions/ssl/linux/run-basic-ssl-cli-tests/action.yml ================================================ name: 'Run Basic SSL CLI Tests - Linux' description: 'Run basic SSL CLI tests on Linux' runs: using: 'composite' steps: - name: Run CLI tests shell: bash run: | set -euo pipefail # navigate to basic SSL test collection directory cd tests/ssl/basic-ssl/collections/badssl echo "basic ssl success" # should pass node ../../../../../packages/bruno-cli/bin/bru.js run ./request.bru --output junit1.xml --insecure --format junit xmllint --xpath 'count(//testsuite[@errors="0"])' junit1.xml | grep -q "^1$" || exit 1 echo "with default/system ca certs" # should pass node ../../../../../packages/bruno-cli/bin/bru.js run ./request.bru --output junit2.xml --format junit xmllint --xpath 'count(//testsuite[@errors="0"])' junit2.xml | grep -q "^1$" || exit 1 # navigate to self-signed SSL test collection directory cd ../self-signed-badssl echo "self-signed ssl with validation disabled" # should pass node ../../../../../packages/bruno-cli/bin/bru.js run ./request.bru --output junit3.xml --insecure --format junit xmllint --xpath 'count(//testsuite[@errors="0"])' junit3.xml | grep -q "^1$" || exit 1 echo "self-signed ssl with default/system ca certs" echo "request will error" # should fail node ../../../../../packages/bruno-cli/bin/bru.js run ./request.bru --output junit4.xml --format junit 2>/dev/null || true xmllint --xpath 'count(//testsuite[@errors="1"])' junit4.xml | grep -q "^1$" || exit 1 ================================================ FILE: .github/actions/ssl/linux/run-custom-ca-certs-cli-tests/action.yml ================================================ name: 'Run Custom CA Certs CLI Tests - Linux' description: 'Run custom CA certs CLI tests on Linux' runs: using: 'composite' steps: - name: Run CLI tests shell: bash run: | set -euo pipefail # navigate to CA certificates test collection directory cd tests/ssl/custom-ca-certs/collection echo "custom valid ca cert" # should pass node ../../../../packages/bruno-cli/bin/bru.js run ./request.bru --output junit1.xml --cacert ../server/certs/ca-cert.pem --ignore-truststore --format junit xmllint --xpath 'count(//testsuite[@errors="0"])' junit1.xml | grep -q "^1$" || exit 1 echo "custom valid ca cert with defaults" # should pass node ../../../../packages/bruno-cli/bin/bru.js run ./request.bru --output junit2.xml --cacert ../server/certs/ca-cert.pem --format junit xmllint --xpath 'count(//testsuite[@errors="0"])' junit2.xml | grep -q "^1$" || exit 1 echo "custom invalid ca cert" echo "request will error" # should fail 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 xmllint --xpath 'count(//testsuite[@errors="1"])' junit3.xml | grep -q "^1$" || exit 1 echo "custom invalid ca cert with defaults" # should pass node ../../../../packages/bruno-cli/bin/bru.js run ./request.bru --output junit4.xml --cacert ../server/certs/ca-key.pem --format junit xmllint --xpath 'count(//testsuite[@errors="0"])' junit4.xml | grep -q "^1$" || exit 1 ================================================ FILE: .github/actions/ssl/linux/run-ssl-e2e-tests/action.yml ================================================ name: 'Run SSL E2E Tests - Linux' description: 'Run SSL E2E tests on Linux' runs: using: 'composite' steps: - name: Run E2E tests shell: bash run: | set -euo pipefail xvfb-run npm run test:e2e:ssl - name: Upload Playwright Report if: ${{ !cancelled() }} uses: actions/upload-artifact@v4 with: name: playwright-report-linux path: playwright-report/ retention-days: 30 ================================================ FILE: .github/actions/ssl/linux/setup-ca-certs/action.yml ================================================ name: 'Setup CA Certificates - Linux' description: 'Setup CA certificates and start test server for custom CA certs tests on Linux' runs: using: 'composite' steps: - name: Setup CA certificates shell: bash run: | set -euo pipefail cd tests/ssl/custom-ca-certs/server echo "running certificate setup" node scripts/generate-certs.js - name: Start test server shell: bash run: | set -euo pipefail cd tests/ssl/custom-ca-certs/server echo "starting server in background" node index.js & echo "server started with PID: $!" ================================================ FILE: .github/actions/ssl/linux/setup-feature-specific-deps/action.yml ================================================ name: 'Setup Custom CA Certs Feature Dependencies - Linux' description: 'Setup feature-specific dependencies for custom CA certs tests on Linux' runs: using: 'composite' steps: - name: Install additional OS dependencies for custom CA certs shell: bash run: | sudo apt-get update sudo apt-get --no-install-recommends install -y \ libglib2.0-0 libnss3 libdbus-1-3 libatk1.0-0 libatk-bridge2.0-0 libcups2 libgtk-3-0 libasound2t64 \ xvfb libxml2-utils sudo chown root /home/runner/work/bruno/bruno/node_modules/electron/dist/chrome-sandbox sudo chmod 4755 /home/runner/work/bruno/bruno/node_modules/electron/dist/chrome-sandbox ================================================ FILE: .github/actions/ssl/macos/run-basic-ssl-cli-tests/action.yml ================================================ name: 'Run Basic SSL CLI Tests - macOS' description: 'Run basic SSL CLI tests on macOS' runs: using: 'composite' steps: - name: Run CLI tests shell: bash run: | set -euo pipefail # navigate to basic SSL test collection directory cd tests/ssl/basic-ssl/collections/badssl echo "basic ssl success" # should pass node ../../../../../packages/bruno-cli/bin/bru.js run ./request.bru --output junit1.xml --insecure --format junit xmllint --xpath 'count(//testsuite[@errors="0"])' junit1.xml | grep -q "^1$" || exit 1 echo "with default/system ca certs" # should pass node ../../../../../packages/bruno-cli/bin/bru.js run ./request.bru --output junit2.xml --format junit xmllint --xpath 'count(//testsuite[@errors="0"])' junit2.xml | grep -q "^1$" || exit 1 # navigate to self-signed SSL test collection directory cd ../self-signed-badssl echo "self-signed ssl with validation disabled" # should pass node ../../../../../packages/bruno-cli/bin/bru.js run ./request.bru --output junit3.xml --insecure --format junit xmllint --xpath 'count(//testsuite[@errors="0"])' junit3.xml | grep -q "^1$" || exit 1 echo "self-signed ssl with default/system ca certs" echo "request will error" # should fail node ../../../../../packages/bruno-cli/bin/bru.js run ./request.bru --output junit4.xml --format junit 2>/dev/null || true xmllint --xpath 'count(//testsuite[@errors="1"])' junit4.xml | grep -q "^1$" || exit 1 ================================================ FILE: .github/actions/ssl/macos/run-custom-ca-certs-cli-tests/action.yml ================================================ name: 'Run Custom CA Certs CLI Tests - macOS' description: 'Run custom CA certs CLI tests on macOS' runs: using: 'composite' steps: - name: Run CLI tests shell: bash run: | set -euo pipefail # navigate to CA certificates test collection directory cd tests/ssl/custom-ca-certs/collection echo "custom valid ca cert" # should pass node ../../../../packages/bruno-cli/bin/bru.js run ./request.bru --output junit1.xml --cacert ../server/certs/ca-cert.pem --ignore-truststore --format junit xmllint --xpath 'count(//testsuite[@errors="0"])' junit1.xml | grep -q "^1$" || exit 1 echo "custom valid ca cert with defaults" # should pass node ../../../../packages/bruno-cli/bin/bru.js run ./request.bru --output junit2.xml --cacert ../server/certs/ca-cert.pem --format junit xmllint --xpath 'count(//testsuite[@errors="0"])' junit2.xml | grep -q "^1$" || exit 1 echo "custom invalid ca cert" echo "request will error" # should fail 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 xmllint --xpath 'count(//testsuite[@errors="1"])' junit3.xml | grep -q "^1$" || exit 1 echo "custom invalid ca cert with defaults" # should pass node ../../../../packages/bruno-cli/bin/bru.js run ./request.bru --output junit4.xml --cacert ../server/certs/ca-key.pem --format junit xmllint --xpath 'count(//testsuite[@errors="0"])' junit4.xml | grep -q "^1$" || exit 1 ================================================ FILE: .github/actions/ssl/macos/run-ssl-e2e-tests/action.yml ================================================ name: 'Run SSL E2E Tests - macOS' description: 'Run SSL E2E tests on macOS' runs: using: 'composite' steps: - name: Run E2E tests shell: bash run: | npm run test:e2e:ssl - name: Upload Playwright Report if: ${{ !cancelled() }} uses: actions/upload-artifact@v4 with: name: playwright-report-macos path: playwright-report/ retention-days: 30 ================================================ FILE: .github/actions/ssl/macos/setup-ca-certs/action.yml ================================================ name: 'Setup CA Certificates - macOS' description: 'Setup CA certificates and start test server for custom CA certs tests on macOS' runs: using: 'composite' steps: - name: Setup CA certificates shell: bash run: | set -euo pipefail cd tests/ssl/custom-ca-certs/server echo "running certificate setup" node scripts/generate-certs.js - name: Start test server shell: bash run: | set -euo pipefail cd tests/ssl/custom-ca-certs/server echo "starting server in background" node index.js & echo "server started with PID: $!" ================================================ FILE: .github/actions/ssl/macos/setup-feature-specific-deps/action.yml ================================================ name: 'Setup Custom CA Certs Feature Dependencies - macOS' description: 'Setup feature-specific dependencies for custom CA certs tests on macOS' runs: using: 'composite' steps: - name: Install additional OS dependencies for custom CA certs shell: bash run: | brew install libxml2 ================================================ FILE: .github/actions/ssl/windows/run-basic-ssl-cli-tests/action.yml ================================================ name: 'Run Basic SSL CLI Tests - Windows' description: 'Run basic SSL CLI tests on Windows' runs: using: 'composite' steps: - name: Run CLI tests shell: pwsh run: | Set-StrictMode -Version Latest $ErrorActionPreference = "Stop" # navigate to basic SSL test collection directory Set-Location tests\ssl\basic-ssl\collections\badssl Write-Host "basic ssl success" # should pass $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" [xml]$xml1 = Get-Content junit1.xml $testsuites1 = if ($xml1.testsuites) { $xml1.testsuites.testsuite } else { $xml1.testsuite } $errorCount1 = ($testsuites1 | Where-Object { $_.errors -eq "0" } | Measure-Object).Count if ($errorCount1 -ne 1) { exit 1 } Write-Host "with default/system ca certs" # should pass $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" [xml]$xml2 = Get-Content junit2.xml $testsuites2 = if ($xml2.testsuites) { $xml2.testsuites.testsuite } else { $xml2.testsuite } $errorCount2 = ($testsuites2 | Where-Object { $_.errors -eq "0" } | Measure-Object).Count if ($errorCount2 -ne 1) { exit 1 } # navigate to self-signed SSL test collection directory Set-Location ..\self-signed-badssl Write-Host "self-signed ssl with validation disabled" # should pass $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" [xml]$xml3 = Get-Content junit3.xml $testsuites3 = if ($xml3.testsuites) { $xml3.testsuites.testsuite } else { $xml3.testsuite } $errorCount3 = ($testsuites3 | Where-Object { $_.errors -eq "0" } | Measure-Object).Count if ($errorCount3 -ne 1) { exit 1 } Write-Host "self-signed ssl with default/system ca certs" Write-Host "request will error" # should fail $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" # Ignore the exit code - we expect this to fail [xml]$xml4 = Get-Content junit4.xml $testsuites4 = if ($xml4.testsuites) { $xml4.testsuites.testsuite } else { $xml4.testsuite } $errorCount4 = ($testsuites4 | Where-Object { $_.errors -eq "1" } | Measure-Object).Count if ($errorCount4 -ne 1) { exit 1 } ================================================ FILE: .github/actions/ssl/windows/run-custom-ca-certs-cli-tests/action.yml ================================================ name: 'Run Custom CA Certs CLI Tests - Windows' description: 'Run custom CA certs CLI tests on Windows' runs: using: 'composite' steps: - name: Run CLI tests shell: pwsh run: | Set-StrictMode -Version Latest $ErrorActionPreference = "Stop" # navigate to CA certificates test collection directory Set-Location tests\ssl\custom-ca-certs\collection Write-Host "custom valid ca cert" # should pass $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" [xml]$xml1 = Get-Content junit1.xml $testsuites1 = if ($xml1.testsuites) { $xml1.testsuites.testsuite } else { $xml1.testsuite } $errorCount1 = ($testsuites1 | Where-Object { $_.errors -eq "0" } | Measure-Object).Count if ($errorCount1 -ne 1) { exit 1 } Write-Host "custom valid ca cert with defaults" # should pass $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" [xml]$xml2 = Get-Content junit2.xml $testsuites2 = if ($xml2.testsuites) { $xml2.testsuites.testsuite } else { $xml2.testsuite } $errorCount2 = ($testsuites2 | Where-Object { $_.errors -eq "0" } | Measure-Object).Count if ($errorCount2 -ne 1) { exit 1 } Write-Host "custom invalid ca cert" Write-Host "request will error" # should fail $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" # Ignore the exit code - we expect this to fail [xml]$xml3 = Get-Content junit3.xml $testsuites3 = if ($xml3.testsuites) { $xml3.testsuites.testsuite } else { $xml3.testsuite } $errorCount3 = ($testsuites3 | Where-Object { $_.errors -eq "1" } | Measure-Object).Count if ($errorCount3 -ne 1) { exit 1 } Write-Host "custom invalid ca cert with defaults" # should pass $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" [xml]$xml4 = Get-Content junit4.xml $testsuites4 = if ($xml4.testsuites) { $xml4.testsuites.testsuite } else { $xml4.testsuite } $errorCount4 = ($testsuites4 | Where-Object { $_.errors -eq "0" } | Measure-Object).Count if ($errorCount4 -ne 1) { exit 1 } ================================================ FILE: .github/actions/ssl/windows/run-ssl-e2e-tests/action.yml ================================================ name: 'Run SSL E2E Tests - Windows' description: 'Run SSL E2E tests on Windows' runs: using: 'composite' steps: - name: Run E2E tests shell: pwsh run: | npm run test:e2e:ssl - name: Upload Playwright Report if: ${{ !cancelled() }} uses: actions/upload-artifact@v4 with: name: playwright-report-windows path: playwright-report/ retention-days: 30 ================================================ FILE: .github/actions/ssl/windows/setup-ca-certs/action.yml ================================================ name: 'Setup CA Certificates - Windows' description: 'Setup CA certificates and start test server for custom CA certs tests on Windows' runs: using: 'composite' steps: - name: Setup CA certificates shell: pwsh run: | Set-StrictMode -Version Latest $ErrorActionPreference = "Stop" Set-Location tests\ssl\custom-ca-certs\server Write-Host "running certificate setup" node scripts/generate-certs.js - name: Start test server shell: pwsh run: | Set-StrictMode -Version Latest Set-Location tests\ssl\custom-ca-certs\server Write-Host "starting server in background" Start-Process -FilePath "node" -ArgumentList "index.js" -PassThru -WindowStyle Hidden ================================================ FILE: .github/actions/tests/run-cli-tests/action.yml ================================================ name: 'Run CLI Tests' description: 'Setup dependencies, start local testbench and run CLI tests' runs: using: 'composite' steps: - name: Run Local Testbench shell: bash run: | npm start --workspace=packages/bruno-tests & sleep 5 - name: Install Test Collection Dependencies shell: bash run: npm ci --prefix packages/bruno-tests/collection - name: Run CLI Tests shell: bash run: | cd packages/bruno-tests/collection node ../../bruno-cli/bin/bru.js run --env Prod --output junit.xml --format junit --sandbox developer ================================================ FILE: .github/actions/tests/run-e2e-tests/action.yml ================================================ name: 'Run E2E Tests' description: 'Setup dependencies, configure environment, and run Playwright E2E tests' inputs: os: description: 'Operating system (ubuntu, macos, windows)' default: 'ubuntu' runs: using: 'composite' steps: - name: Install Test Collection Dependencies shell: bash run: npm ci --prefix packages/bruno-tests/collection - name: Run Playwright Tests (Ubuntu) if: inputs.os == 'ubuntu' shell: bash run: xvfb-run npm run test:e2e - name: Run Playwright Tests if: inputs.os != 'ubuntu' shell: bash run: npm run test:e2e ================================================ FILE: .github/actions/tests/run-unit-tests/action.yml ================================================ name: 'Run Unit Tests' description: 'Setup dependencies and run unit tests for all packages' runs: using: 'composite' steps: - name: Test Package bruno-js shell: bash run: npm run test --workspace=packages/bruno-js - name: Test Package bruno-cli shell: bash run: npm run test --workspace=packages/bruno-cli - name: Test Package bruno-query shell: bash run: npm run test --workspace=packages/bruno-query - name: Test Package bruno-lang shell: bash run: npm run test --workspace=packages/bruno-lang - name: Test Package bruno-schema shell: bash run: npm run test --workspace=packages/bruno-schema - name: Test Package bruno-app shell: bash run: npm run test --workspace=packages/bruno-app - name: Test Package bruno-common shell: bash run: npm run test --workspace=packages/bruno-common - name: Test Package bruno-converters shell: bash run: npm run test --workspace=packages/bruno-converters - name: Test Package bruno-electron shell: bash run: npm run test --workspace=packages/bruno-electron - name: Test Package bruno-requests shell: bash run: npm run test --workspace=packages/bruno-requests - name: Test Package bruno-filestore shell: bash run: npm run test --workspace=packages/bruno-filestore ================================================ FILE: .github/dependabot.yml ================================================ version: 2 updates: - package-ecosystem: "github-actions" directory: "/" schedule: interval: weekly - package-ecosystem: npm directory: "/" schedule: interval: weekly groups: bruno-dependencies: patterns: - "*usebruno*" babel-dependencies: patterns: - "*babel*" fortawesome-dependencies: patterns: - "*fortawesome*" electron-dependencies: patterns: - "*electron*" rollup-dependencies: patterns: - "*rollup*" jest-dependencies: patterns: - "*jest*" ================================================ FILE: .github/scripts/comment-on-flaky-tests.js ================================================ const fs = require('fs'); const { execSync } = require('child_process'); // Check if flaky-tests.json exists if (!fs.existsSync('flaky-tests.json')) { console.log('No flaky-tests.json found'); process.exit(0); } // Get changed files in PR let changedFiles = []; try { changedFiles = execSync('git diff --name-only origin/main...HEAD') .toString() .split('\n') .filter(f => f.endsWith('.spec.ts')); } catch (error) { console.log('Could not determine changed files:', error.message); process.exit(0); } if (changedFiles.length === 0) { console.log('No test files were modified in this PR'); process.exit(0); } // Read flaky tests const flakyTests = JSON.parse(fs.readFileSync('flaky-tests.json', 'utf8')); if (flakyTests.length === 0) { console.log('No flaky/failed tests found'); process.exit(0); } // Find modified flaky tests const modifiedFlakyTests = flakyTests.filter(test => changedFiles.some(file => test.file.includes(file)) ); if (modifiedFlakyTests.length === 0) { console.log('No modified test files are flaky'); process.exit(0); } // Generate comment markdown let comment = '## ⚠️ Warning: You modified flaky/failed test files\n\n'; comment += 'The following test files you modified have reliability issues:\n\n'; modifiedFlakyTests.forEach(test => { const testType = test.status === 'failed' ? '❌ Failed' : '⚠️ Flaky'; comment += `### ${testType}: \`${test.file}\`\n`; comment += `**Test:** ${test.testTitle}\n`; comment += `**Status:** ${test.status}\n`; if (test.retryAttempt > 0) { comment += `**Retry Attempt:** ${test.retryAttempt}\n`; } comment += '\n**To debug locally, run:**\n'; comment += '```bash\n'; comment += `npx playwright test ${test.file} --repeat-each=5 --workers=1\n`; comment += '```\n\n'; }); comment += '---\n'; comment += '**Note:** Flaky tests passed after retrying, failed tests did not pass. '; comment += 'Please investigate and fix the root cause before merging.\n'; // Save comment to file for GitHub Action to post fs.writeFileSync('pr-comment.md', comment); console.log(`Found ${modifiedFlakyTests.length} modified flaky tests`); ================================================ FILE: .github/scripts/detect-flaky-tests.js ================================================ const fs = require('fs'); // Read Playwright JSON report const resultsPath = 'playwright-report/results.json'; if (!fs.existsSync(resultsPath)) { console.log('No Playwright results found at', resultsPath); process.exit(0); } const results = JSON.parse(fs.readFileSync(resultsPath, 'utf8')); // Extract flaky tests // A test is flaky if: status === "passed" AND retry > 0 // A test is failed if: status === "failed" // This means it failed initially but passed on retry OR failed completely const flakyTests = []; function traverseSuites(suites) { for (const suite of suites) { // Process specs in this suite for (const spec of suite.specs || []) { for (const test of spec.tests || []) { // Check each test result for (const result of test.results || []) { // Track two types of problematic tests: // 1. Flaky: passed on a retry attempt (retry > 0) // 2. Failed: failed on all attempts if ((result.status === 'passed' && result.retry > 0) || result.status === 'failed') { flakyTests.push({ file: spec.file, title: spec.title, testTitle: spec.title, line: spec.line, status: result.status, retryAttempt: result.retry }); break; // Only record once per test } } } } // Recursively process nested suites if (suite.suites && suite.suites.length > 0) { traverseSuites(suite.suites); } } } traverseSuites(results.suites || []); // Save flaky tests to JSON fs.writeFileSync('flaky-tests.json', JSON.stringify(flakyTests, null, 2)); // Generate markdown report let markdown = '## ⚠️ Flaky/Failed Tests Detected\n\n'; markdown += 'The following tests are problematic:\n\n'; flakyTests.forEach(test => { const testType = test.status === 'failed' ? '❌ Failed' : '⚠️ Flaky'; markdown += `### ${testType}: \`${test.file}\`\n`; markdown += `- **Test:** ${test.testTitle}\n`; markdown += `- **Status:** ${test.status}\n`; if (test.retryAttempt > 0) { markdown += `- **Retry Attempt:** ${test.retryAttempt}\n`; } markdown += `- **Debug command:**\n`; markdown += '```bash\n'; markdown += `npx playwright test ${test.file} --repeat-each=5 --workers=1\n`; markdown += '```\n\n'; }); fs.writeFileSync('flaky-report.md', markdown); console.log(`Found ${flakyTests.length} flaky/failed tests`); process.exit(flakyTests.length > 0 ? 1 : 0); ================================================ FILE: .github/workflows/flaky-test-detector.yml ================================================ name: Flaky Test Detector on: pull_request: branches: [main] paths: - 'tests/**/*.spec.ts' permissions: contents: read pull-requests: write issues: write checks: write jobs: detect-flaky-tests: name: Detect Flaky Tests runs-on: ubuntu-24.04 timeout-minutes: 60 steps: - name: Checkout code uses: actions/checkout@v6 with: fetch-depth: 0 # Need full history to compare with main - name: Setup Node.js uses: actions/setup-node@v5 with: node-version-file: '.nvmrc' cache: 'npm' - name: Install system dependencies run: | sudo apt-get update sudo apt-get --no-install-recommends install -y \ libglib2.0-0 libnss3 libdbus-1-3 libatk1.0-0 libatk-bridge2.0-0 \ libcups2 libgtk-3-0 libasound2t64 xvfb - name: Install npm dependencies run: | npm ci --legacy-peer-deps sudo chown root /home/runner/work/bruno/bruno/node_modules/electron/dist/chrome-sandbox sudo chmod 4755 /home/runner/work/bruno/bruno/node_modules/electron/dist/chrome-sandbox - name: Install test collection dependencies run: npm ci --prefix packages/bruno-tests/collection - name: Build libraries run: | npm run build:graphql-docs npm run build:bruno-query npm run build:bruno-common npm run sandbox:bundle-libraries --workspace=packages/bruno-js npm run build:bruno-converters npm run build:bruno-requests npm run build:schema-types npm run build:bruno-filestore - name: Run Playwright tests run: xvfb-run npm run test:e2e continue-on-error: true # Continue even if tests fail - name: Detect flaky tests id: detect run: node .github/scripts/detect-flaky-tests.js continue-on-error: true # Don't fail workflow if flaky tests found - name: Check modified flaky tests id: check-modified run: node .github/scripts/comment-on-flaky-tests.js continue-on-error: true - name: Post PR comment if: hashFiles('pr-comment.md') != '' uses: actions/github-script@v7 with: script: | const fs = require('fs'); const comment = fs.readFileSync('pr-comment.md', 'utf8'); // Check if we already commented const { data: comments } = await github.rest.issues.listComments({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number }); const botComment = comments.find(c => c.user.type === 'Bot' && c.body.includes('Warning: You modified flaky/failed test files') ); if (botComment) { // Update existing comment await github.rest.issues.updateComment({ owner: context.repo.owner, repo: context.repo.repo, comment_id: botComment.id, body: comment }); } else { // Create new comment await github.rest.issues.createComment({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number, body: comment }); } - name: Upload flaky test artifacts if: always() uses: actions/upload-artifact@v6 with: name: flaky-test-results path: | flaky-tests.json flaky-report.md playwright-report/ retention-days: 30 ================================================ FILE: .github/workflows/lint-checks.yml ================================================ name: Lint Checks on: workflow_dispatch: push: branches: [main, 'release/v*'] pull_request: branches: [main, 'release/v*'] jobs: lint: name: Lint Check runs-on: ubuntu-latest permissions: contents: read steps: - uses: actions/checkout@v6 - name: Setup Node Dependencies uses: ./.github/actions/common/setup-node-deps with: skip-build: 'true' - name: Lint Check run: npm run lint env: ESLINT_PLUGIN_DIFF_COMMIT: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.ref || 'main' }} ================================================ FILE: .github/workflows/npm-bru-cli.yml ================================================ name: Bru CLI Tests (npm) on: workflow_dispatch: inputs: build: description: 'Test Bru CLI (npm)' required: true default: 'true' # Assign permissions for unit tests to be reported. # See https://github.com/dorny/test-reporter/issues/168 permissions: statuses: write checks: write contents: write pull-requests: write actions: write jobs: test: name: CLI Tests strategy: matrix: os: [ubuntu-latest, macos-latest] runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v6 - uses: actions/setup-node@v5 with: node-version-file: '.nvmrc' - name: Install Bru CLI from NPM run: npm install -g @usebruno/cli - name: Display Bru CLI Version run: bru --version - name: Run tests run: | cd packages/bruno-tests/collection npm install bru run --env Prod --output junit.xml --format junit --sandbox developer - name: Publish Test Report uses: dorny/test-reporter@v2 if: success() || failure() with: name: Test Report path: packages/bruno-tests/collection/junit.xml reporter: java-junit ================================================ FILE: .github/workflows/ssl-tests.yml ================================================ name: SSL Tests on: push: branches: [main] pull_request: branches: [main] jobs: tests-for-linux: name: SSL Tests - Linux timeout-minutes: 60 runs-on: ubuntu-latest permissions: checks: write pull-requests: write contents: read steps: - uses: actions/checkout@v6 - name: Setup Node Dependencies uses: ./.github/actions/common/setup-node-deps - name: Setup Feature Dependencies uses: ./.github/actions/ssl/linux/setup-feature-specific-deps - name: Setup CA Certificates uses: ./.github/actions/ssl/linux/setup-ca-certs - name: Run Basic SSL CLI Tests uses: ./.github/actions/ssl/linux/run-basic-ssl-cli-tests - name: Run Custom CA Certs CLI Tests uses: ./.github/actions/ssl/linux/run-custom-ca-certs-cli-tests - name: Run Custom CA Certs E2E Tests uses: ./.github/actions/ssl/linux/run-ssl-e2e-tests tests-for-macos: name: SSL Tests - macOS timeout-minutes: 60 runs-on: macos-latest permissions: checks: write pull-requests: write contents: read steps: - uses: actions/checkout@v6 - name: Setup Node Dependencies uses: ./.github/actions/common/setup-node-deps - name: Setup Feature Dependencies uses: ./.github/actions/ssl/macos/setup-feature-specific-deps - name: Setup CA Certificates uses: ./.github/actions/ssl/macos/setup-ca-certs - name: Run Basic SSL CLI Tests uses: ./.github/actions/ssl/macos/run-basic-ssl-cli-tests - name: Run Custom CA Certs CLI Tests uses: ./.github/actions/ssl/macos/run-custom-ca-certs-cli-tests - name: Run Custom CA Certs E2E Tests uses: ./.github/actions/ssl/macos/run-ssl-e2e-tests tests-for-windows: name: SSL Tests - Windows timeout-minutes: 60 runs-on: windows-latest permissions: checks: write pull-requests: write contents: read steps: - uses: actions/checkout@v6 - name: Setup Node Dependencies uses: ./.github/actions/common/setup-node-deps - name: Setup CA Certificates uses: ./.github/actions/ssl/windows/setup-ca-certs - name: Run Basic SSL CLI Tests uses: ./.github/actions/ssl/windows/run-basic-ssl-cli-tests - name: Run Custom CA Certs CLI Tests uses: ./.github/actions/ssl/windows/run-custom-ca-certs-cli-tests - name: Run Custom CA Certs E2E Tests uses: ./.github/actions/ssl/windows/run-ssl-e2e-tests ================================================ FILE: .github/workflows/tests.yml ================================================ name: Tests on: workflow_dispatch: push: branches: [main, 'release/v*'] pull_request: branches: [main, 'release/v*'] jobs: unit-test: name: Unit Tests timeout-minutes: 60 runs-on: ubuntu-latest permissions: contents: read steps: - uses: actions/checkout@v6 - name: Setup Node Dependencies uses: ./.github/actions/common/setup-node-deps - name: Run Unit Tests uses: ./.github/actions/tests/run-unit-tests cli-test: name: CLI Tests runs-on: ubuntu-latest permissions: checks: write pull-requests: write contents: read steps: - uses: actions/checkout@v6 - name: Setup Node Dependencies uses: ./.github/actions/common/setup-node-deps - name: Run CLI Tests uses: ./.github/actions/tests/run-cli-tests - name: Publish Test Report uses: EnricoMi/publish-unit-test-result-action@v2 if: always() with: check_name: CLI Test Results files: packages/bruno-tests/collection/junit.xml comment_mode: always e2e-test: name: Playwright E2E Tests timeout-minutes: 60 runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v6 - name: Install System Dependencies (Ubuntu) run: | sudo apt-get update sudo apt-get --no-install-recommends install -y \ libglib2.0-0 libnss3 libdbus-1-3 libatk1.0-0 libatk-bridge2.0-0 libcups2 libgtk-3-0 libasound2t64 \ xvfb - name: Setup Node Dependencies uses: ./.github/actions/common/setup-node-deps - name: Configure Chrome Sandbox run: | sudo chown root node_modules/electron/dist/chrome-sandbox sudo chmod 4755 node_modules/electron/dist/chrome-sandbox - name: Run playwright Tests uses: ./.github/actions/tests/run-e2e-tests with: os: ubuntu - name: Upload Playwright Report uses: actions/upload-artifact@v6 if: ${{ !cancelled() }} with: name: playwright-report path: playwright-report/ retention-days: 30 ================================================ FILE: .gitignore ================================================ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. # dependencies bun.lockb node_modules yarn.lock pnpm-lock.yaml .pnp .pnp.js bun.lockb bun.lock # testing coverage # production build chrome-extension chrome-extension.pem chrome-extension.crx bruno.zip *.zip # misc .DS_Store *.pem # debug npm-debug.log* yarn-debug.log* yarn-error.log* # local env files .env.local .env.development.local .env.test.local .env.production.local # next.js /renderer /renderer/.next/ /renderer/out/ /test-results/ /playwright-report/ /playwright/.cache/ #dev editor bruno.iml .idea .vscode .cursor .claude .codex .agents .agent skills-lock.json # Playwright /blob-report/ # Development plan files CLAUDE.md AGENTS.md *.plan.md # packages dist packages/bruno-filestore/dist packages/bruno-requests/dist packages/bruno-schema-types/dist packages/bruno-converters/dist ================================================ FILE: .husky/pre-commit ================================================ npx nano-staged ================================================ FILE: .nvmrc ================================================ v22.12.0 ================================================ FILE: CODING_STANDARDS.md ================================================ # Bruno Coding Standards - 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. ## General Style Rules - Use 2 spaces for indentation. No tabs, just spaces – keeps everything neat and uniform. - Stick to single quotes for strings. For JSX/TSX attributes, use double quotes (e.g., ) to follow React conventions. - Always add semicolons at the end of statements. It's like putting a period at the end of a sentence – clarity matters. - JSX is enabled, so feel free to use it where it makes sense. ## Punctuation and Spacing - No trailing commas. Keep it clean, no extra commas hanging around. - Always use parentheses around parameters in arrow functions. Even for single params – consistency is key. - For multiline constructs, put opening braces on the same line, and ensure consistency. Minimum 2 elements for multiline. - No newlines inside function parentheses. Keep 'em tight. - Space before and after the arrow in arrow functions. `() => {}` is good. - No space between function name and parentheses. `func()` not `func ()`. - Semicolons go at the end of the line, not on a new line. - No strict max length – write readable code, not cramped lines. - Multiple expressions per line in JSX are fine – flexibility is nice. Remember, these rules are here to make our codebase harmonious. If something doesn't fit perfectly, let's chat about it. Happy coding! 🚀 ## Tests - Add tests for any new functionality or meaningful changes. If code is added, removed, or significantly modified, corresponding tests should be updated or created. - 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. - 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. - 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. - Keep tests readable and maintainable. Optimise for clarity over cleverness. Name tests descriptively, keep setup minimal, and avoid unnecessary abstraction. - Aim for tests that fail usefully. When a test fails, it should clearly indicate what behaviour broke and why. - 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. - Ensure tests are deterministic and reproducible. No randomness, timing dependencies, or environment-specific assumptions without explicit control. - Avoid overfitting tests to current behaviour if future flexibility matters. Only assert what needs to be true, not incidental details. - 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. - Tests should be fast enough to run continuously. Avoid long-running operations unless absolutely necessary; prefer lightweight fixtures and isolated units. ## UI Specific instructions ### React - 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 - Styled Components are used as wrappers to define both self and children components style, tailwind classes are used specifically for layout based styles. - Styled Component CSS might also change layout but tailwind classes shouldn't define colors. - MUST: Prefer custom hooks for business logic, data fetching, and side-effects. - MUST: Avoid `useEffect` unless absolutely needed. Prefer derived state, event handlers. - SHOULD: Memoize only when necessary (`useMemo`/`useCallback`), and prefer moving logic into hooks first. - MUST: Do not use namespace access for hooks in app code (e.g., `React.useCallback`, `React.useMemo`, `React.useState`). Import hooks directly. - Correct: `import { useCallback, useMemo, useState } from "react";` - Avoid: `import * as React from "react";` then `React.useCallback(...)` - Add `data-testid` to testable elements for Playwright - Co-locate utilities that are truly component-specific next to the component, otherwise place shared items under a common folder ## Readability and Abstractions - Avoid abstractions unless the exact same code is being used in more than 3 places. - Names for functions need to be concise and descriptive. - Add in JSDoc comments to add more details to the abstractions if needed. - 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. - Avoid single line abstractions where all that's being done is increasing the call stack with one additional function. - Add in meaningful comments instead of obvious ones where complex code flow is explained properly. ================================================ FILE: contributing.md ================================================ **English** | [Українська](docs/contributing/contributing_ua.md) | [Русский](docs/contributing/contributing_ru.md) | [Türkçe](docs/contributing/contributing_tr.md) | [Deutsch](docs/contributing/contributing_de.md) | [Français](docs/contributing/contributing_fr.md) | [Português (BR)](docs/contributing/contributing_pt_br.md) | [한국어](docs/contributing/contributing_kr.md) | [বাংলা](docs/contributing/contributing_bn.md) | [Español](docs/contributing/contributing_es.md) | [Italiano](docs/contributing/contributing_it.md) | [Română](docs/contributing/contributing_ro.md) | [Polski](docs/contributing/contributing_pl.md) | [简体中文](docs/contributing/contributing_cn.md) | [正體中文](docs/contributing/contributing_zhtw.md) | [日本語](docs/contributing/contributing_ja.md) | [हिंदी](docs/contributing/contributing_hi.md) | [Dutch](docs/contributing/contributing_nl.md) | [فارسی](docs/contributing/contributing_fa.md) ## Let's make Bruno better, together!! We are happy that you are looking to improve Bruno. Below are the guidelines to run Bruno on your computer. ### Technology Stack Bruno is built using React and Electron. Libraries we use - CSS - Tailwind - Code Editors - Codemirror - State Management - Redux - Icons - Tabler Icons - Forms - formik - Schema Validation - Yup - Request Client - axios - Filesystem Watcher - chokidar - i18n - i18next > [!IMPORTANT] > You would need [Node v22.x or the latest LTS version](https://nodejs.org/en/). We use npm workspaces in the project ## Development Bruno is a desktop app. Below are the instructions to run Bruno. > Note: We use React for the frontend and rsbuild for build and dev server. ## Install Dependencies ```bash # use nodejs 22 version nvm use # install deps npm i --legacy-peer-deps ``` ### Local Development #### Build packages ##### Option 1 ```bash # build packages npm run build:graphql-docs npm run build:bruno-query npm run build:bruno-common npm run build:bruno-converters npm run build:bruno-requests npm run build:schema-types npm run build:bruno-filestore # bundle js sandbox libraries npm run sandbox:bundle-libraries --workspace=packages/bruno-js ``` ##### Option 2 ```bash # install dependencies and setup npm run setup ``` #### Run the app ##### Option 1 ```bash # run react app (terminal 1) npm run dev:web # run electron app (terminal 2) npm run dev:electron ``` ##### Option 2 ```bash # run electron and react app concurrently npm run dev ``` #### Customize Electron `userData` path If `ELECTRON_USER_DATA_PATH` env-variable is present and its development mode, then `userData` path is modified accordingly. e.g. ```sh ELECTRON_USER_DATA_PATH=$(realpath ~/Desktop/bruno-test) npm run dev:electron ``` This will create a `bruno-test` folder on your Desktop and use it as the `userData` path. ### Troubleshooting You 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. ```shell # Delete node_modules in sub-directories find ./ -type d -name "node_modules" -print0 | while read -d $'\0' dir; do rm -rf "$dir" done # Delete package-lock in sub-directories find . -type f -name "package-lock.json" -delete ``` ### Testing ```bash # run bruno-schema tests npm run test --workspace=packages/bruno-schema # run bruno-query tests npm run test --workspace=packages/bruno-query # run bruno-common tests npm run test --workspace=packages/bruno-common # run bruno-converters tests npm run test --workspace=packages/bruno-converters # run bruno-app tests npm run test --workspace=packages/bruno-app # run bruno-electron tests npm run test --workspace=packages/bruno-electron # run bruno-lang tests npm run test --workspace=packages/bruno-lang # run bruno-toml tests npm run test --workspace=packages/bruno-toml # run tests over all workspaces npm test --workspaces --if-present ``` ### Raising Pull Requests - Please keep the PR's small and focused on one thing - Please follow the format of creating branches - feature/[feature name]: This branch should contain changes for a specific feature - Example: feature/dark-mode - bugfix/[bug name]: This branch should contain only bug fixes for a specific bug - Example bugfix/bug-1 ================================================ FILE: docs/contributing/contributing_bn.md ================================================ [English](../../contributing.md) ## আসুন ব্রুনোকে আরও ভালো করি, একসাথে!! আমরা খুশি যে আপনি ব্রুনোর উন্নতি করতে চাইছেন। নীচে আপনার কম্পিউটারে ব্রুনো ইনষ্টল করার নির্দেশিকা রয়েছে৷। ### Technology Stack (প্রযুক্তি স্ট্যাক) ব্রুনো Next.js এবং React ব্যবহার করে নির্মিত। এছাড়াও আমরা একটি ডেস্কটপ সংস্করণ পাঠাতে ইলেক্ট্রন ব্যবহার করি (যা স্থানীয় সংগ্রহ সমর্থন করে) নিম্ন লিখিত লাইব্রেরি আমরা ব্যবহার করি - - CSS - Tailwind - Code Editors - Codemirror - State Management - Redux - Icons - Tabler Icons - Forms - formik - Schema Validation - Yup - Request Client - axios - Filesystem Watcher - chokidar ### Dependencies (নির্ভরতা) আপনার প্রয়োজন হবে [নোড v18.x বা সর্বশেষ LTS সংস্করণ](https://nodejs.org/en/) এবং npm 8.x। আমরা প্রকল্পে npm ওয়ার্কস্পেস ব্যবহার করি । ## Development ব্রুনো একটি ডেস্কটপ অ্যাপ হিসেবে তৈরি করা হচ্ছে। আপনাকে একটি টার্মিনালে Next.js অ্যাপটি চালিয়ে অ্যাপটি লোড করতে হবে এবং তারপরে অন্য টার্মিনালে ইলেক্ট্রন অ্যাপটি চালাতে হবে। ### Dependencies (নির্ভরতা) - NodeJS v18 ### Local Development ```bash # nodejs 18 সংস্করণ ব্যবহার করুন nvm use # নির্ভরতা ইনস্টল করুন npm i --legacy-peer-deps # গ্রাফকিউএল ডক্স তৈরি করুন npm run build:graphql-docs # ব্রুনো কোয়েরি তৈরি করুন npm run build:bruno-query # NextJs অ্যাপ চালান (টার্মিনাল 1) npm run dev:web # ইলেক্ট্রন অ্যাপ চালান (টার্মিনাল 2) npm run dev:electron ``` ### Troubleshooting (সমস্যা সমাধান) আপনি যখন 'npm install' চালান তখন আপনি একটি 'অসমর্থিত প্ল্যাটফর্ম' ত্রুটির সম্মুখীন হতে পারেন৷ এটি ঠিক করতে, আপনাকে `node_modules` এবং `package-lock.json` মুছে ফেলতে হবে এবং `npm install` চালাতে হবে। এটি অ্যাপটি চালানোর জন্য প্রয়োজনীয় সমস্ত প্যাকেজ ইনস্টল করবে যাতে এই ত্রুটি ঠিক হয়ে যেতে পারে । ```shell # সাব-ডিরেক্টরিতে নোড_মডিউল মুছুন find ./ -type d -name "node_modules" -print0 | while read -d $'\0' dir; do rm -rf "$dir" done # সাব-ডিরেক্টরিতে প্যাকেজ-লক মুছুন find . -type f -name "package-lock.json" -delete ``` ### Testing (পরীক্ষা) ```bash # ব্রুনো-স্কিমা পরীক্ষা চালান npm test --workspace=packages/bruno-schema # সমস্ত কর্মক্ষেত্রে পরীক্ষা চালান npm test --workspaces --if-present ``` ### Raising Pull Request (পুল অনুরোধ উত্থাপন) - অনুগ্রহ করে PR এর আকার ছোট রাখুন এবং একটি বিষয়ে ফোকাস করুন। - অনুগ্রহ করে শাখা তৈরির বিন্যাস অনুসরণ করুন। - বৈশিষ্ট্য/[ফিচারের নাম]: এই শাখায় একটি নির্দিষ্ট বৈশিষ্ট্যের জন্য পরিবর্তন থাকতে হবে। - উদাহরণ: বৈশিষ্ট্য/ডার্ক-মোড। - বাগফিক্স/[বাগ নাম]: এই শাখায় একটি নির্দিষ্ট বাগ-এর জন্য শুধুমাত্র বাগ ফিক্স থাকা উচিত। - উদাহরণ বাগফিক্স/বাগ-1। ================================================ FILE: docs/contributing/contributing_cn.md ================================================ [English](../../contributing.md) ## 让我们一起改进 Bruno! 很高兴看到您考虑改进 Bruno。以下是获取 Bruno 并在您的电脑上运行它的规则和指南。 ### 使用的技术 Bruno 基于 NextJs 和 React 构建。我们使用 Electron 来封装桌面版本。 我们使用的库包括: - CSS - Tailwind - 代码编辑器 - Codemirror - 状态管理 - Redux - 图标 - Tabler Icons - 表单 - formik - 模式验证 - Yup - 请求客户端 - axios - 文件系统监视器 - chokidar ### 依赖项 您需要 [Node v20.x 或最新的 LTS 版本](https://nodejs.org/en/) 和 npm 8.x。我们在这个项目中也使用 npm 工作区(_npm workspaces_)。 ## 开发 Bruno 是作为一个 _client lourd(重客户端)_ 应用程序开发的。您需要在一个终端中启动 nextjs 来加载应用程序,然后在另一个终端中启动 Electron 应用程序。 ### 依赖项 - NodeJS v18 ### 本地开发 ```bash # 使用 node 版本 18 nvm use # 安装依赖项 npm i --legacy-peer-deps # 构建 graphql 文档 npm run build:graphql-docs # 构建 bruno 查询 npm run build:bruno-query # 启动 next(终端 1) npm run dev:web # 启动重客户端(终端 2) npm run dev:electron ``` ### 故障排除 在运行 npm install 时,您可能会遇到 Unsupported platform 错误。为了解决这个问题,请删除 node_modules 目录和 package-lock.json 文件,然后再次运行 npm install。这应该会安装运行应用程序所需的所有包。 ```shell # 删除子目录中的 node_modules 目录 find ./ -type d -name "node_modules" -print0 | while read -d $'\0' dir; do rm -rf "$dir" done # 删除子目录中的 package-lock.json 文件 find . -type f -name "package-lock.json" -delete ``` ### 测试 ```bash # 运行 bruno-schema 测试 npm test --workspace=packages/bruno-schema # 在所有工作区上运行测试 npm test --workspaces --if-present ``` ### 提交 Pull Request - 请保持 PR 精简并专注于单一目标 - 请遵循分支命名格式: - feature/[feature name]:该分支应包含特定功能 - 例如:feature/dark-mode - bugfix/[bug name]:该分支应仅包含特定 bug 的修复 - 例如:bugfix/bug-1 ================================================ FILE: docs/contributing/contributing_de.md ================================================ [English](../../contributing.md) ## Lass uns Bruno noch besser machen, gemeinsam!! Ich freue mich, dass Du Bruno verbessern möchtest. Hier findest Du eine Anleitung, mit der Du Bruno auf Deinem Computer einrichten kannst. ### Technologie Stack Bruno ist mit Next.js und React erstellt. Außerdem benötigen wir electron für die Desktop Version (die lokale Sammlungen unterstützt). Bibliotheken die wir benutzen - CSS - Tailwind - Code Editoren - Codemirror - State Management - Redux - Icons - Tabler Icons - Formulare - formik - Schema Validierung - Yup - Request Client - axios - Dateisystem Watcher - chokidar ### Abhängigkeiten Du 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. ### Lass uns coden Eine Anleitung zum Ausführen einer lokalen Entwicklungsumgebung findest Du in [development.md](docs/development_de.md). ### Pull Request erstellen - Bitte halte die PRs klein und begrenzt auf eine Sache - Bitte halte Dich beim Erstellen eines Branches an das folgende Format - feature/[feature name]: Dieser Branch soll Änderungen für ein bestimmtes Feature enthalten - Beispiel: feature/dark-mode - bugfix/[bug name]: Dieser Branch soll ausschließlich Bugfixes für einen bestimmten Bug enthalten - Beispiel: bugfix/bug-1 ## Entwicklung Bruno 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. ### Abhängigkeiten - NodeJS v22 ### Lokales Entwickeln ```bash # use nodejs 22 version nvm use # install deps npm i --legacy-peer-deps # build graphql docs npm run build:graphql-docs # build bruno query npm run build:bruno-query # run next app (terminal 1) npm run dev:web # run electron app (terminal 2) npm run dev:electron ``` ### Troubleshooting Es 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. ```shell # Delete node_modules in sub-directories find ./ -type d -name "node_modules" -print0 | while read -d $'\0' dir; do rm -rf "$dir" done # Delete package-lock in sub-directories find . -type f -name "package-lock.json" -delete ``` ### Testen ```bash # Führen Sie Bruno-Schema-Tests aus npm test --workspace=packages/bruno-schema # Führen Sie Tests für alle Arbeitsbereiche durch npm test --workspaces --if-present ``` ================================================ FILE: docs/contributing/contributing_es.md ================================================ [Inglés](../../contributing.md) ## ¡Juntos, hagamos a Bruno mejor! Estamos encantados de que quieras ayudar a mejorar Bruno. A continuación encontrarás las instrucciones para empezar a trabajar con Bruno en tu computadora. ### Tecnologías utilizadas Bruno está construido con React y Electron Librerías que utilizamos: - CSS - Tailwind CSS - Editores de código - CodeMirror - Manejo del estado - Redux - Íconos - Tabler Icons - Formularios - formik - Validación de esquemas - Yup - Cliente de peticiones - axios - Monitor del sistema de archivos - chokidar - i18n (internacionalización) - i18next ### Dependencias > [!IMPORTANT] > 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 ## Desarrollo Bruno es una aplicación de escritorio. A continuación se detallan las instrucciones paso a paso para ejecutar Bruno. > Nota: Utilizamos React para el frontend y rsbuild para el servidor de desarrollo. ### Instalar dependencias ```bash # Use la versión 22.x o LTS (Soporte a Largo Plazo) de Node.js nvm use 22.11.0 # instalar las dependencias npm i --legacy-peer-deps ``` > ¿Por qué `--legacy-peer-deps`?: Fuerza la instalación ignorando conflictos en dependencias “peer”, evitando errores de árbol de dependencias. ### Desarrollo local #### Construir paquetes ##### Opción 1 ```bash # construir paquetes npm run build:graphql-docs npm run build:bruno-query npm run build:bruno-common npm run build:bruno-converters npm run build:bruno-requests # empaquetar bibliotecas JavaScript del entorno de pruebas aislado npm run sandbox:bundle-libraries --workspace=packages/bruno-js ``` ##### Opción 2 ```bash # instalar dependencias y configurar el entorno npm run setup ``` #### Ejecutar la aplicación ```bash # ejecutar aplicación react (terminal 1) npm run dev:web # ejecutar aplicación electron (terminal 2) npm run dev:electron ``` ##### Opción 1 ```bash # ejecutar aplicación react (terminal 1) npm run dev:web # ejecutar aplicación electron (terminal 2) npm run dev:electron ``` ##### Opción 2 ```bash # ejecutar aplicación electron y react de forma concurrente npm run dev ``` #### Personalizar la ruta `userData` de Electron Si 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. ejemplo: ```sh ELECTRON_USER_DATA_PATH=$(realpath ~/Desktop/bruno-test) npm run dev:electron ``` Esto creará una carpeta llamada `bruno-test` en tu escritorio y la usará como la ruta userData. ### Solución de problemas Es 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. ```sh # Elimina la carpeta node_modules en los subdirectorios find ./ -type d -name "node_modules" -print0 | while read -d $'\0' dir; do rm -rf "$dir" done # Elimina el archivo package-lock en los subdirectorios find . -type f -name "package-lock.json" -delete ``` ### Pruebas #### Pruebas individuales ```bash # ejecutar pruebas de bruno-app npm run test --workspace=packages/bruno-app # ejecutar pruebas de bruno-electron npm run test --workspace=packages/bruno-electron # ejecutar pruebas de bruno-cli npm run test --workspace=packages/bruno-cli # ejecutar pruebas de bruno-common npm run test --workspace=packages/bruno-common # ejecutar pruebas de bruno-converters npm run test --workspace=packages/bruno-converters # ejecutar pruebas de bruno-schema npm run test --workspace=packages/bruno-schema # ejecutar pruebas de bruno-query npm run test --workspace=packages/bruno-query # ejecutar pruebas de bruno-js npm run test --workspace=packages/bruno-js # ejecutar pruebas de bruno-lang npm run test --workspace=packages/bruno-lang # ejecutar pruebas de bruno-toml npm run test --workspace=packages/bruno-toml ``` #### Pruebas en conjunto ```bash # ejecutar pruebas en todos los espacios de trabajo npm test --workspaces --if-present ``` ### Crea un Pull Request - Por favor, mantén los Pull Request pequeños y enfocados en una sola cosa. - Por favor, sigue el siguiente formato para la creación de ramas: - feature/[nombre de la funcionalidad]: Esta rama debe contener los cambios para una funcionalidad específica. - Ejemplo: feature/dark-mode - bugfix/[nombre del error]: Esta rama debe contener solo correcciones de errores para un error específico. - Ejemplo: bugfix/bug-1 ================================================ FILE: docs/contributing/contributing_fa.md ================================================ [English](../../contributing.md) ## با هم، Bruno را بهتر می‌کنیم! خوشحالم که قصد دارید Bruno را بهبود ببخشید. در ادامه قوانین و راهنماها برای راه‌اندازی Bruno روی سیستم شما آورده شده است. ### فناوری‌های استفاده‌شده به فارسی برونو Bruno با استفاده از Next.js و React ساخته شده است. همچنین از Electron برای بسته‌بندی نسخه دسکتاپ (که امکان مجموعه‌های محلی را فراهم می‌کند) استفاده می‌کنیم. کتابخانه‌هایی که استفاده می‌کنیم: - CSS - Tailwind استایل - Codemirror - ویرایشگر کد - Redux - مدیریت وضعیت - Tabler Icons - آیکون‌ها - formik - فرم‌ها - Yup اعتبارسنجی اسکیمـا - axios - کلاینت درخواست - chokidar - پایش‌گر سیستم فایل ### پیش‌نیازها شما به [نود v20.x یا اخرین نسخه پایدار](https://nodejs.org/en/) و npm 8.x نیاز دارید. در این پروژه از فضای کاری npm (npm workspaces) استفاده می‌کنیم. ### شروع به کدنویسی برای راه‌اندازی محیط توسعه محلی به فایل [مستندات توسعه](docs/development_fa.md) مراجعه کنید: ### ارسال Pull Request 1 - لطفاً Pull Requestها (PR) را کوتاه و متمرکز نگه دارید و تنها یک هدف مشخص را دنبال کنند.
2 - لطفاً از فرمت نام‌گذاری شاخه‌ها استفاده کنید: - feature/[name]: این شاخه باید شامل یک قابلیت مشخص باشد. - feature/dark-mode : مثال - bugfix/[name]: این شاخه باید تنها شامل رفع یک باگ مشخص باشد. - bugfix/bug-1 : مثال ## توسعه به فارسی برونو یا Bruno به‌صورت یک اپلیکیشن «سنگین» توسعه داده می‌شود. برای اجرا باید ابتدا Next.js را در یک پنجره ترمینال اجرا کنید و سپس اپلیکیشن Electron را در پنجره ترمینال دیگری راه‌اندازی نمایید. ### نیازمندی توسعه - NodeJS v18 ### اجرای محلی ```bash # از ورژن NodeJS 18 استفاده کنید nvm use # نصب وابستگی‌ها npm i --legacy-peer-deps # ساخت مستندات GraphQL npm run build:graphql-docs # ساخت bruno-query npm run build:bruno-query # اجرای اپ Next (ترمینال 1) npm run dev:web # اجرای اپ Electron (ترمینال 2) npm run dev:electron ``` ### عیب‌یابی ممکن است هنگام اجرای `npm install` خطای `Unsupported platform` ببینید. برای رفع این مشکل، پوشه `node_modules` و فایل `package-lock.json` را حذف کرده و سپس دوباره `npm install` را اجرا کنید. این کار معمولاً همه پکیج‌های لازم را نصب می‌کند. ```shell # حذف پوشه node_modules در زیردایرکتوری‌ها find ./ -type d -name "node_modules" -print0 | while read -d $'\0' dir; do rm -rf "$dir" done # حذف فایل package-lock.json در زیردایرکتوری‌ها find . -type f -name "package-lock.json" -delete ``` ### تست‌ها ```bash # اجرای تست‌های schema مربوط به bruno npm test --workspace=packages/bruno-schema # اجرای تست‌ها در همه فضاهای کاری (در صورت وجود) npm test --workspaces --if-present ``` ================================================ FILE: docs/contributing/contributing_fr.md ================================================ [English](../../contributing.md) ## Ensemble, améliorons Bruno ! Je 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. ### Technologies utilisées Bruno est basé sur NextJs et React. Nous utilisons aussi Electron pour embarquer la version ordinateur (ce qui permet les collections locales). Les librairies que nous utilisons : - CSS - Tailwind - Code Editors - Codemirror - State Management - Redux - Icons - Tabler Icons - Forms - formik - Schema Validation - Yup - Request Client - axios - Filesystem Watcher - chokidar ### Dépendances Vous 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. ## Développement Bruno 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. ### Dépendances - NodeJS v18 ### Développement local ```bash # utiliser node en version 18 nvm use # installation des dépendances npm i --legacy-peer-deps # construction des docs graphql npm run build:graphql-docs # construction de bruno query npm run build:bruno-query # construction de bruno common npm run build:bruno-common # démarrage de next (terminal 1) npm run dev:web # démarrage du client lourd (terminal 2) npm run dev:electron ``` ### Dépannage Vous 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. ```shell # Efface les répertoires node_modules dans les sous-répertoires find ./ -type d -name "node_modules" -print0 | while read -d $'\0' dir; do rm -rf "$dir" done # Efface les fichiers package-lock.json dans les sous-répertoires find . -type f -name "package-lock.json" -delete ``` ### Tests ```bash # exécuter des tests de schéma bruno npm test --workspace=packages/bruno-schema # exécuter des tests sur tous les espaces de travail npm test --workspaces --if-present ``` ### Ouvrir une Pull Request - Merci de conserver les PR minimes et focalisées sur un seul objectif - Merci de suivre le format de nom des branches : - feature/[feature name]: Cette branche doit contenir une fonctionnalité spécifique - Exemple : feature/dark-mode - bugfix/[bug name]: Cette branche doit contenir seulement une solution pour un bug spécifique - Exemple : bugfix/bug-1 ================================================ FILE: docs/contributing/contributing_hi.md ================================================ [English](../../contributing.md) ## आइए मिलकर Bruno को बेहतर बनाएं !! हमें खुशी है कि आप Bruno को बेहतर बनाना चाहते हैं। Bruno को अपने कंप्यूटर पर लाना शुरू करने के लिए दिशानिर्देश नीचे दिए गए हैं। ### टेक्नोलॉजी स्टैक Bruno को Next.js और React का उपयोग करके बनाया गया है। हम डेस्कटॉप संस्करण को शिप करने के लिए इलेक्ट्रॉन का भी उपयोग करते हैं (जो स्थानीय संग्रह का समर्थन करता है) Libraries जिनका हम उपयोग करते हैं - CSS - Tailwind - कोड संपादक - Codemirror - State Management - Redux - Icons - Tabler Icons - Forms - formik - Schema Validation - Yup - Request Client - axios - Filesystem Watcher - chokidar ### निर्भरताएँ आपको [Node v20.x या नवीनतम LTS संस्करण](https://nodejs.org/en/) और npm 8.x की आवश्यकता होगी। हम प्रोजेक्ट में npm वर्कस्पेस का उपयोग करते हैं ## डेवलपमेंट Bruno को एक डेस्कटॉप ऐप के रूप में बनाया किया जा रहा है। आपको Next.js ऐप को एक टर्मिनल में चलाकर ऐप को लोड करना होगा और फिर इलेक्ट्रॉन ऐप को दूसरे टर्मिनल में चलाना होगा। ### लोकल डेवलपमेंट ```bash # nodejs 18 संस्करण का उपयोग करें nvm use # डिपेंडेंसी इनस्टॉल करे npm i --legacy-peer-deps # पैकेज बिल्ड करें npm run build:graphql-docs npm run build:bruno-query npm run build:bruno-common npm run build:bruno-converters npm run build:bruno-requests # Next.js ऐप चलाएँ (टर्मिनल 1 पर) npm run dev:web # इलेक्ट्रॉन ऐप चलाएँ (टर्मिनल 2 पर) npm run dev:electron ``` ### समस्या निवारण जब आप `npm इंस्टॉल` चलाते हैं तो आपको `असमर्थित प्लेटफ़ॉर्म` त्रुटि का सामना करना पड़ सकता है। इसे ठीक करने के लिए, आपको `node_modules` और `package-lock.json` को हटाना होगा और `npm install` चलाना होगा। इसमें ऐप चलाने के लिए आवश्यक सभी आवश्यक पैकेज इंस्टॉल होने चाहिए। ```shell # सब-डायरेक्टरी में node_modules डिलीट करे find ./ -type d -name "node_modules" -print0 | while read -d $'\0' dir; do rm -rf "$dir" done # सब-डायरेक्टरी में package-lock डिलीट करे find . -type f -name "package-lock.json" -delete ``` ### परिक्षण ```bash # ब्रूनो-स्कीमा परीक्षण चलाएँ npm test --workspace=packages/bruno-schema # सभी कार्यस्थानों पर परीक्षण चलाएँ npm test --workspaces --if-present ``` ### पुल अनुरोध प्रक्रिया - कृपया PR को छोटा रखें और एक चीज़ पर केंद्रित रखें - कृपया शाखाएँ बनाने के प्रारूप का पालन करें - feature/[feature name]: इस शाखा में किसी विशिष्ट सुविधा के लिए परिवर्तन होने चाहिए - उदाहरण: feature/dark-mode - bugfix/[bug name]: इस शाखा में केवल विशिष्ट बग के लिए बग फिक्स शामिल होने चाहिए - उदाहरण bugfix/bug-1 ================================================ FILE: docs/contributing/contributing_it.md ================================================ [English](../../contributing.md) ## Insieme, miglioriamo Bruno! Sono felice di vedere che hai intenzione di migliorare Bruno. Di seguito, troverai le regole e le guide per ripristinare Bruno sul tuo computer. ### Tecnologie utilizzate Bruno è costruito utilizzando Next.js e React. Utilizziamo anche Electron per incorporare la versione desktop (che consente raccolte locali). Le librerie che utilizziamo sono: - CSS - Tailwind - Code Editors - Codemirror - State Management - Redux - Icons - Tabler Icons - Forms - formik - Schema Validation - Yup - Request Client - axios - Filesystem Watcher - chokidar ### Dependences Hai 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. ### Iniziamo a codificare Si prega di fare riferimento alla [documentazione di sviluppo](docs/development_it.md) per le istruzioni su come avviare l'ambiente di sviluppo locale. ### Aprire una richiesta di pull (Pull Request) - Si prega di mantenere le Pull Request (PR) brevi e concentrate su un singolo obiettivo. - Si prega di seguire il formato di denominazione dei rami. - feature/[feature name]: Questo ramo dovrebbe contenere una specifica funzionalità. - Esempio: feature/dark-mode - bugfix/[bug name]: Questo ramo dovrebbe contenere solo una soluzione per un bug specifico. - Esempio: bugfix/bug-1 ## Sviluppo Bruno è 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. ### Sviluppo - NodeJS v18 ### Sviluppo locale ```bash # use nodejs 18 version nvm use # install deps npm i --legacy-peer-deps # build graphql docs npm run build:graphql-docs # build bruno query npm run build:bruno-query # run next app (terminal 1) npm run dev:web # run electron app (terminal 2) npm run dev:electron ``` ### Risoluzione dei problemi Potresti 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. ```shell # delete node_modules in sub-directories find ./ -type d -name "node_modules" -print0 | while read -d $'\0' dir; do rm -rf "$dir" done # delete package-lock in sub-directories find . -type f -name "package-lock.json" -delete ``` ### Tests ```bash # esegui i test dello schema bruno npm test --workspace=packages/bruno-schema # esegui test su tutti gli spazi di lavoro npm test --workspaces --if-present ``` ================================================ FILE: docs/contributing/contributing_ja.md ================================================ [English](../../contributing.md) ## 一緒に Bruno をよりよいものにしていきましょう!! Bruno を改善していただけるのは歓迎です。以下はあなたの環境で Bruno を起動するためのガイドラインです。 ### 技術スタック Bruno は Next.js と React で作られています。デスクトップアプリ(ローカルのコレクションに対応しています)には electron も使用しています。 使用ライブラリ - CSS - Tailwind - Code Editors - Codemirror - State Management - Redux - Icons - Tabler Icons - Forms - formik - Schema Validation - Yup - Request Client - axios - Filesystem Watcher - chokidar ### 依存関係 [Node v20.x もしくは最新の LTS バージョン](https://nodejs.org/en/)と npm 8.x が必要です。プロジェクトに npm ワークスペースを使用しています。 ## 開発 Bruno はデスクトップアプリとして開発されています。一つのターミナルで Next.js アプリを立ち上げ、もう一つのターミナルで electron アプリを立ち上げてアプリを読み込む必要があります。 ### ローカル環境での開発 ```bash # use nodejs 18 version nvm use # install deps npm i --legacy-peer-deps # build packages npm run build:graphql-docs npm run build:bruno-query npm run build:bruno-common npm run build:bruno-converters npm run build:bruno-requests # run next app (terminal 1) npm run dev:web # run electron app (terminal 2) npm run dev:electron ``` ### トラブルシューティング `npm install`を実行すると、`Unsupported platform`エラーに遭遇することがあります。これを直すためには、`node_modules`と`package-lock.json`を削除し、`npm install`を実行しなおす必要があります。これにより、アプリを動かすのに必要なパッケージがすべてインストールされます。 ```shell # Delete node_modules in sub-directories find ./ -type d -name "node_modules" -print0 | while read -d $'\0' dir; do rm -rf "$dir" done # Delete package-lock in sub-directories find . -type f -name "package-lock.json" -delete ``` ### テストを動かすには ```bash # ブルーノスキーマのテストを実行します npm test --workspace=packages/bruno-schema # すべてのワークスペースでテストを実行します npm test --workspaces --if-present ``` ### プルリクエストの手順 - プルリクエストは小規模で、一つのことにフォーカスしたものにしてください。 - 以下のフォーマットに従ってブランチを作ってください。 - feature/[feature name]: このブランチには特定の機能に対する変更を含んでください。 - 例: feature/dark-mode - bugfix/[bug name]: このブランチには特定のバグに対する修正のみを含むようにしてください。 - 例: bugfix/bug-1 ================================================ FILE: docs/contributing/contributing_kr.md ================================================ [English](../../contributing.md) ## 함께 Bruno를 더 좋게 만들어요!! 우리는 여러분이 Bruno를 발전시키기 위해 노력해주셔서 기쁩니다. 다음은 여러분의 컴퓨터에서 Bruno를 불러오는 가이드라인입니다. ### 기술 스택 Bruno는 Next.js와 React로 구축되었습니다. 또한, (로컬 컬렉션을 지원하는) 데스크톱 버전을 제공하기 위해 electron을 사용합니다. 우리가 사용하는 라이브러리 - CSS - Tailwind - Code Editors - Codemirror - State Management - Redux - Icons - Tabler Icons - Forms - formik - Schema Validation - Yup - Request Client - axios - Filesystem Watcher - chokidar ### 의존성 [Node v20.x 혹은 최신 LTS version](https://nodejs.org/en/)과 npm 8.x 버전이 필요합니다. 우리는 이 프로젝트에서 npm workspaces를 사용합니다. ## 개발 Bruno는 데스크톱 앱으로 개발되고 있습니다. 한 터미널에서 Next.js를 실행하여 앱을 로드한 다음 다른 터미널에서 electron 앱을 실행해야합니다. ### 로컬 개발 ```bash # nodejs 18 버전 사용 nvm use # 의존성 설치 npm i --legacy-peer-deps # packages 빌드 npm run build:graphql-docs npm run build:bruno-query npm run build:bruno-common npm run build:bruno-converters npm run build:bruno-requests # next 앱 실행 (1번 터미널) npm run dev:web # electron 앱 실행 (2번 터미널) npm run dev:electron ``` ### 트러블 슈팅 `npm install`을 실행할 때, `Unsupported platform` 에러를 마주칠 수 있습니다. 이것을 고치기 위해서는 `node_modules`와 `package-lock.json`을 삭제하고 `npm install`을 실행해야 합니다. 그러면 앱을 실행하기 위해 필요한 패키지들이 모두 설치됩니다. ```shell # 하위 디렉토리에 있는 node_modules 삭제 find ./ -type d -name "node_modules" -print0 | while read -d $'\0' dir; do rm -rf "$dir" done # 하위 디렉토리에 있는 package-lock 삭제 find . -type f -name "package-lock.json" -delete ``` ### 테스팅 ```bash # bruno-schema 테스트 실행 npm test --workspace=packages/bruno-schema # 모든 작업 공간에서 테스트 실행 npm test --workspaces --if-present ``` ### Pull Requests 요청 - PR을 작게 유지하고 한가지에 집중해주세요. - 브랜치를 생성하는 형식을 따라주세요. - feature/[feature name]: 이 브랜치는 특정 기능에 대한 변경사항이 포함되어야합니다. - 예시: feature/dark-mode - bugfix/[bug name]: 이 브랜치는 특정 버그에 대한 버그 수정만 포함되어야합니다. - 예시: bugfix/bug-1 ================================================ FILE: docs/contributing/contributing_nl.md ================================================ [English](../../contributing.md) ## Laten we Bruno samen beter maken !! We zijn blij dat je Bruno wilt verbeteren. Hieronder staan de richtlijnen om Bruno op je computer op te zetten. ### Technologiestack Bruno is gebouwd met Next.js en React. We gebruiken ook Electron om een desktopversie te leveren (die lokale collecties ondersteunt). Bibliotheken die we gebruiken: - CSS - Tailwind - Code Editors - Codemirror - State Management - Redux - Iconen - Tabler Icons - Formulieren - formik - Schema Validatie - Yup - Request Client - axios - Bestandsysteem Watcher - chokidar ### Afhankelijkheden Je 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. ## Ontwikkeling Bruno 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. ### Lokale Ontwikkeling ```bash # gebruik voorgeschreven node versie nvm use # installeer afhankelijkheden npm i --legacy-peer-deps # build pakketten npm run build:graphql-docs npm run build:bruno-query npm run build:bruno-common npm run build:bruno-converters npm run build:bruno-requests # draai next app (terminal 1) npm run dev:web # draai electron app (terminal 2) npm run dev:electron ``` ### Problemen oplossen Je 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. ```shell # Verwijder node_modules in subdirectories find ./ -type d -name "node_modules" -print0 | while read -d $'\0' dir; do rm -rf "$dir" done # Verwijder package-lock in subdirectories find . -type f -name "package-lock.json" -delete ``` ### Testen ```bash # voer bruno-schema tests uit npm test --workspace=packages/bruno-schema # voer tests uit over alle werkruimten npm test --workspaces --if-present ``` ### Pull Requests indienen - Houd de PR's klein en gefocust op één ding - Volg het formaat voor het aanmaken van branches - feature/[feature naam]: Deze branch moet wijzigingen voor een specifieke functie bevatten - Voorbeeld: feature/dark-mode - bugfix/[bug naam]: Deze branch moet alleen bugfixes voor een specifieke bug bevatten - Voorbeeld: bugfix/bug-1 ================================================ FILE: docs/contributing/contributing_pl.md ================================================ [English](../../contributing.md) ## Wspólnie uczynijmy Bruno lepszym !! Cieszymy się, że chcesz udoskonalić Bruno. Poniżej znajdziesz wskazówki, jak rozpocząć pracę z Bruno na Twoim komputerze. ### Stos Technologiczny Bruno jest zbudowane przy użyciu Next.js i React. Używamy również electron do stworzenia wersji desktopowej (która obsługuje lokalne kolekcje) Biblioteki, których używamy - CSS - Tailwind - Edytory Kodu - Codemirror - Zarządzanie Stanem - Redux - Ikony - Tabler Icons - Formularze - formik - Walidacja Schematu - Yup - Klient Zapytań - axios - Obserwator Systemu Plików - chokidar ### Zależności Będziesz potrzebować [Node v20.x lub najnowszej wersji LTS](https://nodejs.org/en/) oraz npm 8.x. W projekcie używamy npm workspaces ## Rozwój Bruno 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. ### Zależności - NodeJS v18 ### Lokalny Rozwój ```bash # użyj wersji nodejs 18 nvm use # zainstaluj zależności npm i --legacy-peer-deps # zbuduj dokumentację graphql npm run build:graphql-docs # zbuduj zapytanie bruno npm run build:bruno-query # uruchom aplikację next (terminal 1) npm run dev:web # uruchom aplikację electron (terminal 2) npm run dev:electron ``` ### Rozwiązywanie Problemów Moż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. ```shell # Usuń node_modules w podkatalogach find ./ -type d -name "node_modules" -print0 | while read -d $'\0' dir; do rm -rf "$dir" done # Usuń package-lock w podkatalogach find . -type f -name "package-lock.json" -delete ``` ### Testowanie ```bash # uruchom testy bruno-schema npm test --workspace=packages/bruno-schema # uruchom testy we wszystkich przestrzeniach roboczych npm test --workspaces --if-present ``` ### Tworzenie Pull Request - Prosimy, aby PR były małe i skoncentrowane na jednej rzeczy - Prosimy przestrzegać formatu tworzenia gałęzi - feature/[nazwa funkcji]: Ta gałąź powinna zawierać zmiany dotyczące konkretnej funkcji - Przykład: feature/dark-mode - bugfix/[nazwa błędu]: Ta gałąź powinna zawierać tylko poprawki dla konkretnego błędu - Przykład bugfix/bug-1 ================================================ FILE: docs/contributing/contributing_pt_br.md ================================================ [English](../../contributing.md) ## Vamos tornar o Bruno melhor, juntos!! Estamos 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. ### Stack de Tecnologias O 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). Bibliotecas que utilizamos: - CSS - Tailwind - Editor de Código - Codemirror - Gerenciador de Estado - Redux - Ícones - Tabler Icons - Formulários - formik - Validador de Schema - Yup - Cliente de Requisições - axios - Monitor de Arquivos - chokidar ### Dependências Você 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. ## Desenvolvimento Bruno 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. ### Dependências - NodeJS v18 ### Desenvolvimento Local ```bash # use nodejs 18 version nvm use # install deps npm i --legacy-peer-deps # build graphql docs npm run build:graphql-docs # build bruno query npm run build:bruno-query # run next app (terminal 1) npm run dev:web # run electron app (terminal 2) npm run dev:electron ``` ### Troubleshooting Você 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. ```shell # delete node_modules in sub-directories find ./ -type d -name "node_modules" -print0 | while read -d $'\0' dir; do rm -rf "$dir" done # delete package-lock in sub-directories find . -type f -name "package-lock.json" -delete ``` ### Testando ```bash # executar testes do bruno-schema npm test --workspace=packages/bruno-schema # executar testes em todos os ambientes de trabalho npm test --workspaces --if-present ``` ### Envio de Pull Request - Por favor, mantenha os PRs pequenos e focados em uma única coisa. - Siga o formato de criação de branches. - feature/[nome da funcionalidade]: Esta branch deve conter alterações para uma funcionalidade específica. - Exemplo: feature/dark-mode - bugfix/[nome do bug]: Esta branch deve conter apenas correções para um bug específico. - Exemplo: bugfix/bug-1 ================================================ FILE: docs/contributing/contributing_ro.md ================================================ [English](../../contributing.md) ## Haideţi să îmbunătățim Bruno, împreună!! Ne 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. ### Stack-ul tehnologic Bruno este construit cu Next.js și React. De asemenea, folosim electron pentru a livra o versiune desktop (care poate folosi colecții locale) Bibliotecile pe care le folosim - CSS - Tailwind - Editori de cod - Codemirror - Management de condiție - Redux - Icoane - Tabler Icons - Formulare - formik - Validarea schemelor - Yup - Cererile client - axios - Observatorul sistemului de fișiere - chokidar ### Dependențele Veț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 ## Dezvoltarea Bruno 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. ```shell # folosiți nodejs versiunea 18 nvm use # instalați dependențele npm i --legacy-peer-deps # construiți documente graphql npm run build:graphql-docs # construiți bruno query npm run build:bruno-query # rulați aplicația next (terminal 1) npm run dev:web # rulați aplicația electron (terminal 2) npm run dev:electron ``` ### Depanare Este 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. ```shell # Ștergeți node_modules din subdirectoare find ./ -type d -name "node_modules" -print0 | while read -d $'\0' dir; do rm -rf "$dir" done # Ștergeți package-lock din subdirectoare find . -type f -name "package-lock.json" -delete ``` ### Testarea ```shell # executați teste bruno-schema npm test --workspace=packages/bruno-schema # executați teste peste toate spațiile de lucru npm test --workspaces --if-present ``` ### Crearea unui Pull Request - Vă rugăm să păstrați PR-urile mici și concentrate pe un singur lucru - Vă rugăm să urmați formatul de creare a branchurilor - feature/[Numele funcției]: Acest branch ar trebui să conțină modificări pentru o funcție anumită - Exemplu: feature/dark-mode - bugfix/[Numele eroarei]: Acest branch ar trebui să conţină numai remedieri pentru o eroare anumită - Exemplu bugfix/bug-1 ================================================ FILE: docs/contributing/contributing_ru.md ================================================ [English](../../contributing.md) ## Давайте вместе сделаем Бруно лучше!!! Я рад, что Вы хотите усовершенствовать bruno. Ниже приведены рекомендации по запуску bruno на вашем компьютере. ### Стек Bruno построен с использованием Next.js и React. Мы также используем electron для поставки десктопной версии ( которая поддерживает локальные коллекции ) Библиотеки, которые мы используем - CSS - Tailwind - Редакторы кода - Codemirror - Управление состоянием - Redux - Иконки - Tabler Icons - Формы - formik - Валидация схем - Yup - Запросы клиента - axios - Наблюдатель за файловой системой - chokidar ### Зависимости Вам потребуется [Node v20.x или последняя версия LTS](https://nodejs.org/en/) и npm 8.x. В проекте мы используем рабочие пространства npm ### Приступим к коду Пожалуйста, обратитесь к [development_ru.md](docs/development_ru.md) для получения инструкций по запуску локальной среды разработки. ### Создание Pull Request - Пожалуйста, пусть PR будет небольшим и сфокусированным на одной вещи - Пожалуйста, соблюдайте формат создания веток - feature/[название функции]: Эта ветка должна содержать изменения для конкретной функции - Пример: feature/dark-mode - bugfix/[название ошибки]: Эта ветка должна содержать только исправления для конкретной ошибки - Пример bugfix/bug-1 ## Разработка Bruno разрабатывается как десктопное приложение. Необходимо загрузить приложение, запустив приложение Next.js в одном терминале, а затем запустить приложение electron в другом терминале. ### Зависимости - NodeJS v18 ### Локальная разработка ```bash # используйте nodejs 18 версии nvm use # установите зависимости npm i --legacy-peer-deps # билд документации по graphql npm run build:graphql-docs # билд bruno query npm run build:bruno-query # запустить next приложение ( терминал 1 ) npm run dev:web # запустить приложение electron ( терминал 2 ) npm run dev:electron ``` ### Устранение неисправностей При запуске `npm install` может возникнуть ошибка `Unsupported platform`. Чтобы исправить это, необходимо удалить `node_modules` и `package-lock.json` и запустить `npm install`. В результате будут установлены все пакеты, необходимые для работы приложения. ```shell # Удаление node_modules в подкаталогах find ./ -type d -name "node_modules" -print0 | while read -d $'\0' dir; do rm -rf "$dir" done # Удаление package-lock в подкаталогах find . -type f -name "package-lock.json" -delete ``` ### Тестирование ```bash # запустите тесты bruno-schema npm test --workspace=packages/bruno-schema # запустите тесты во всех рабочих пространствах npm test --workspaces --if-present ``` ================================================ FILE: docs/contributing/contributing_sk.md ================================================ ## Urobme bruno lepším, spoločne !! Sme radi, že chcete zlepšiť bruno. Nižšie sú uvedené pokyny, ako začať s výchovou bruno na vašom počítači. ### Technologický zásobník Bruno je vytvorené pomocou Next.js a React. Na dodávanie desktopovej verzie (ktorá podporuje lokálne kolekcie) používame aj electron. Balíčky, ktoré používame: - CSS - Tailwind - Editory kódu - Codemirror - Správa stavu - Redux - Ikony - Tabler Icons - Formuláre - formik - Overovanie schém - Yup - Klient požiadaviek - axios - Sledovač súborového systému - chokidar ### Závislosti Budete 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 ## Vývoj Bruno 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. ### Závislosti - NodeJS v18 ### Miestny vývoj ```bash # použite verziu nodejs 18 nvm use # nainštalovať balíčky npm i --legacy-peer-deps # zostaviť balíčky npm run build:graphql-docs npm run build:bruno-query npm run build:bruno-common npm run build:bruno-converters npm run build:bruno-requests # spustite ďalšiu aplikáciu (terminál 1) npm run dev:web # spustite aplikáciu electron (terminál 2) npm run dev:electron ``` ### Riešenie problémov Pri 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. ```shell # Odstrániť node_modules v podadresároch find ./ -type d -name "node_modules" -print0 | while read -d $'\0' dir; do rm -rf "$dir" done # Odstráňte package-lock v podadresároch find . -type f -name "package-lock.json" -delete ``` ### Testovanie ````bash # spustiť bruno-schema testy npm test --workspace=packages/bruno-schema # spustiť testy vo všetkých pracovných priestoroch npm test --workspaces --if-present ``` ### Vyrobenie Pull Request - Prosím, aby PR boli malé a zamerané na jednu vec - Prosím, dodržujte formát vytvárania vetiev - feature/[názov funkcie]: Táto vetva by mala obsahovať zmeny pre konkrétnu funkciu - Príklad: feature/dark-mode - bugfix/[názov chyby]: Táto vetva by mala obsahovať iba opravy konkrétnej chyby - Príklad: bugfix/bug-1 ================================================ FILE: docs/contributing/contributing_tr.md ================================================ [English](../../contributing.md) ## Bruno'yu birlikte daha iyi hale getirelim!!! bruno'yu geliştirmek istemenizden mutluluk duyuyoruz. Aşağıda, bruno'yu bilgisayarınıza getirmeye başlamak için yönergeler bulunmaktadır. ### Kullanılan Teknolojiler Bruno, 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 Kullandığımız kütüphaneler - CSS - Tailwind - Kod Düzenleyiciler - Codemirror - Durum Yönetimi - Redux - Iconlar - Tabler Icons - Formlar - formik - Şema Doğrulama - Yup - İstek İstemcisi - axios - Dosya Sistemi İzleyicisi - chokidar ### Bağımlılıklar [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 ## Gelişim Bruno 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. ### Bağımlılıklar - NodeJS v18 ### Yerel Geliştirme ```bash # nodejs 18 sürümünü kullan nvm use # deps yükleyin npm i --legacy-peer-deps # graphql dokümanlarını oluştur npm run build:graphql-docs # bruno sorgusu oluştur npm run build:bruno-query # sonraki uygulamayı çalıştır (terminal 1) npm run dev:web # electron uygulamasını çalıştır (terminal 2) npm run dev:electron ``` ### Sorun Giderme `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. ```shell # Alt dizinlerdeki node_modules öğelerini silme find ./ -type d -name "node_modules" -print0 | while read -d $'\0' dir; do rm -rf "$dir" done # Alt dizinlerdeki paket kilidini silme find . -type f -name "package-lock.json" -delete ``` ### Test ```bash # bruno-schema testlerini çalıştır npm test --workspace=packages/bruno-schema # tüm çalışma alanlarında testleri çalıştır npm test --workspaces --if-present ``` ### Pull Request Oluşturma - Lütfen PR'ları küçük tutun ve tek bir şeye odaklanın - Lütfen şube oluşturma formatını takip edin - feature/[özellik adı]: Bu dal belirli bir özellik için değişiklikler içermelidir - Örnek: feature/dark-mode - bugfix/[hata adı]: Bu dal yalnızca belirli bir hata için hata düzeltmeleri içermelidir - Örnek bugfix/bug-1 ================================================ FILE: docs/contributing/contributing_ua.md ================================================ [English](../../contributing.md) ## Давайте зробимо Bruno краще, разом !! Я дуже радий що Ви бажаєте покращити Bruno. Нижче наведені вказівки як розпочати розробку Bruno на Вашому комп'ютері. ### Стек технологій Bruno побудований на Next.js та React. Також для десктопної версії (яка підтримує локальні колекції) використовується Electron Бібліотеки, які ми використовуємо - CSS - Tailwind - Редактори коду - Codemirror - Керування станом - Redux - Іконки - Tabler Icons - Форми - formik - Валідація по схемі - Yup - Клієнт запитів - axios - Спостерігач за файловою системою - chokidar ### Залежності Вам знадобиться [Node v20.x або остання LTS версія](https://nodejs.org/en/) та npm 8.x. Ми використовуєм npm workspaces в цьому проекті ### Починаєм писати код Будь ласка, зверніться до [development_ua.md](docs/development_ua.md) за інструкціями щодо запуску локального середовища розробки. ### Створення Pull Request-ів - Будь ласка, робіть PR-и маленькими і сфокусованими на одній речі - Будь ласка, слідуйте формату назв гілок - feature/[назва feature]: Така гілка має містити зміни лише щодо конкретної feature - Приклад: feature/dark-mode - bugfix/[назва баґу]: Така гілка має містити лише виправлення конкретного багу - Приклад: bugfix/bug-1 ## Розробка Bruno розробляється як декстопний застосунок. Вам потрібно запустити Next.js в одній сесії терміналу, та запустити застосунок Electron в іншій сесії терміналу. ### Залежності - NodeJS v18 ### Локальна розробка ```bash # Використовуйте nodejs 18-ї версії nvm use # встановіть залежності npm i --legacy-peer-deps # зберіть документацію graphql npm run build:graphql-docs # зберіть bruno query npm run build:bruno-query # запустіть додаток next (термінал 1) npm run dev:web # запустіть додаток електрон (термінал 2) npm run dev:electron ``` ### Усунення несправностей Ви можете зтикнутись із помилкою `Unsupported platform` коли запускаєте `npm install`. Щоб усунути цю проблему, вам потрібно видалити `node_modules` та `package-lock.json`, і тоді запустити `npm install`. Це має встановити всі потрібні для запуску додатку пекеджі. ```shell # Видаліть node_modules в піддиректоріях find ./ -type d -name "node_modules" -print0 | while read -d $'\0' dir; do rm -rf "$dir" done # Видаліть package-lock в піддиректоріях find . -type f -name "package-lock.json" -delete ``` ### Тестування ```bash # запустити тести bruno-schema npm test --workspace=packages/bruno-schema # запустити тести у всіх робочих просторах npm test --workspaces --if-present ``` ================================================ FILE: docs/contributing/contributing_zhtw.md ================================================ [English](../../contributing.md) ## 讓我們一起來讓 Bruno 變得更好! 我們很高興您希望一同改善 Bruno。以下是在您的電腦上開始運行 Bruno 的規則及指南。 ### 技術細節 Bruno 使用 Next.js 和 React 構建。我們使用 Electron 來封裝及發佈桌面版本。 我們使用的函式庫: - CSS - Tailwind - 程式碼編輯器 - Codemirror - 狀態管理 - Redux - Icons - Tabler Icons - 表單 - formik - 結構驗證- Yup - 請求用戶端 - axios - 檔案系統監測 - chokidar ### 依賴關係 您需要使用 [Node v20.x 或最新的 LTS 版本](https://nodejs.org/en/) 和 npm 8.x。我們在這個專案中使用 npm 工作區(_npm workspaces_)。 ## 開發 Bruno 正以桌面應用程式的形式開發。您需要在一個終端機中執行 Next.js 來載入應用程式,然後在另一個終端機中執行 electron 應用程式。 ### 開發依賴 - NodeJS v18 ### 本地開發 ```bash # 使用 nodejs 第 18 版 nvm use # 安裝相依套件(使用--legacy-peer-deps 解決套件相依性問題) npm i --legacy-peer-deps # 建立 graphql 文件 npm run build:graphql-docs # 建立 bruno 查詢 npm run build:bruno-query # 執行 next 應用程式(終端機 1) npm run dev:web # 執行 electron 應用程式(終端機 2) npm run dev:electron ``` ### 故障排除 在執行 `npm install` 時,您可能會遇到 `Unsupported platform` 的錯誤訊息。爲了解決這個問題,您需要刪除 `node_modules` 資料夾和 `package-lock.json` 檔案,然後再執行一次 `npm install`。這應該能重新安裝應用程式所需的套件。 ```shell # 刪除子資料夾中的 node_modules 資料夾 find ./ -type d -name "node_modules" -print0 | while read -d $'\0' dir; do rm -rf "$dir" done # 刪除子資料夾中的 package-lock.json 檔案 find . -type f -name "package-lock.json" -delete ``` ### 測試 ```bash # 執行布魯諾架構測試 npm test --workspace=packages/bruno-schema # 對所有工作區執行測試 npm test --workspaces --if-present ``` ### 發送 Pull Request - 請保持 PR 精簡並專注於一個目標 - 請遵循建立分支的格式: - feature/[feature name]:該分支應包含特定功能的更改 - 範例:feature/dark-mode - bugfix/[bug name]:該分支應僅包含特定 bug 的修復 - 範例:bugfix/bug-1 ================================================ FILE: docs/playwright-testing-guide.md ================================================ # Playwright Testing Guide for Bruno This guide explains how to create and run Playwright test cases for the Bruno application using the UI. ## Table of Contents - [Overview](#overview) - [Prerequisites](#prerequisites) - [Creating Tests Using Codegen](#creating-tests-using-codegen) - [Manual Test Creation](#manual-test-creation) - [Test Structure and Organization](#test-structure-and-organization) - [Available Test Fixtures](#available-test-fixtures) - [Running Tests](#running-tests) - [Best Practices](#best-practices) - [Examples](#examples) - [Troubleshooting](#troubleshooting) ## Overview Bruno 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. ## Prerequisites - Node.js installed - All dependencies installed (`npm install`) - Electron app can be built and run ## Creating Tests Using Codegen The easiest way to create tests is using Playwright's codegen feature, which records your UI interactions and generates test code. ### Using the Built-in Codegen Script ```bash # Generate a test with a specific name npm run test:codegen my-new-test # Generate a test without specifying a name (will prompt for input) npm run test:codegen ``` ### What Happens During Codegen 1. The Electron app launches automatically 2. Playwright Inspector opens in a separate window 3. You interact with the Bruno UI 4. Actions are recorded and converted to test code 5. The generated test file is saved in `e2e-tests/` ### Codegen Workflow 1. **Start Recording**: Run the codegen command 2. **Interact with UI**: Perform the actions you want to test 3. **Add Assertions**: Use the inspector to add assertions 4. **Save Test**: The test file is automatically generated 5. **Review and Refine**: Edit the generated test as needed ## Manual Test Creation You can also create tests manually by following the established patterns. ### Basic Test Structure ```typescript import { test, expect } from '../../playwright'; test('Test description', async ({ page }) => { // Test steps here await page.getByLabel('Some Label').click(); // Assertions await expect(page.getByText('Expected Text')).toBeVisible(); }); ``` ### Test with Temporary Data ```typescript import { test, expect } from '../../playwright'; test('Test with temporary data', async ({ page, createTmpDir }) => { // Create temporary directory for test data const testDir = await createTmpDir('test-collection'); // Test steps await page.getByLabel('Create Collection').click(); await page.getByLabel('Name').fill('test-collection'); await page.getByLabel('Location').fill(testDir); // Assertions await expect(page.getByText('test-collection')).toBeVisible(); }); ``` ## Test Structure and Organization ### Directory Structure ``` e2e-tests/ ├── 001-sanity-tests/ # Basic functionality tests │ ├── 001-home-screen.spec.ts │ └── 002-create-new-collection-and-new-request.spec.ts ├── 002-feature-tests/ # Specific feature tests ├── 003-integration-tests/ # Complex workflow tests └── bruno-testbench/ # Test utilities and helpers ``` ### Naming Conventions - **Files**: Use descriptive names with `.spec.ts` extension - **Tests**: Use clear, descriptive test names - **Folders**: Use numbered prefixes for ordering ### Test File Template ```typescript import { test, expect } from '../../playwright'; test.describe('Feature Name', () => { test('should perform specific action', async ({ page }) => { // Arrange // Act // Assert }); test('should handle error case', async ({ page }) => { // Test error scenarios }); }); ``` ## Available Test Fixtures The Bruno Playwright setup provides several custom fixtures: ### Core Fixtures - `page`: Main page for testing - `context`: Browser context - `electronApp`: Electron application instance ### Utility Fixtures - `createTmpDir`: Creates temporary directories for test data - `newPage`: Creates a new page instance - `pageWithUserData`: Page with custom user data - `launchElectronApp`: Launches a new Electron app instance - `reuseOrLaunchElectronApp`: Reuses existing app or launches new one ### Using Fixtures ```typescript test('Test with multiple fixtures', async ({ page, createTmpDir, electronApp }) => { const testDir = await createTmpDir('test-data'); // Your test logic here }); ``` ## Running Tests ### Basic Commands ```bash # Run all tests npm run test:e2e # Run specific test file npx playwright test e2e-tests/001-sanity-tests/001-home-screen.spec.ts # Run tests in a specific folder npx playwright test e2e-tests/001-sanity-tests/ ``` ### Advanced Options ```bash # Run with UI mode (for debugging) npx playwright test --ui # Run in headed mode (see browser) npx playwright test --headed # Run with specific browser npx playwright test --project="Bruno Electron App" # Run with debugging npx playwright test --debug # Run with trace recording npx playwright test --trace on ``` ### CI/CD Integration ```bash # Install browsers for CI npx playwright install # Run tests in CI mode npm run test:e2e ``` ## Best Practices ### 1. Use Semantic Selectors **Preferred:** ```typescript await page.getByRole('button', { name: 'Create' }).click(); await page.getByLabel('Collection Name').fill('test'); await page.getByText('Success message').toBeVisible(); ``` **Avoid:** ```typescript await page.locator('.btn-primary').click(); await page.locator('#collection-name').fill('test'); ``` ### 2. Create Isolated Tests Each test should be independent and not rely on other tests: ```typescript test('should create collection', async ({ page, createTmpDir }) => { const testDir = await createTmpDir('collection-test'); // Test creates its own data await page.getByLabel('Create Collection').click(); await page.getByLabel('Name').fill('test-collection'); await page.getByLabel('Location').fill(testDir); // Clean up happens automatically via createTmpDir }); ``` ### 3. Add Meaningful Assertions Always verify the expected outcomes: ```typescript test('should save request successfully', async ({ page }) => { // Arrange await page.getByLabel('Create Collection').click(); // Act await page.getByRole('button', { name: 'Save' }).click(); // Assert await expect(page.getByText('Request saved successfully')).toBeVisible(); await expect(page.getByRole('tab', { name: 'GET request' })).toBeVisible(); }); ``` ### 4. Handle Async Operations ```typescript test('should wait for network requests', async ({ page }) => { // Wait for specific network request await page.waitForResponse((response) => response.url().includes('/api/endpoint')); // Or wait for element to be stable await page.waitForSelector('[data-testid="loading"]', { state: 'hidden' }); }); ``` ### 5. Use Test Data Management ```typescript test('should work with test data', async ({ page, createTmpDir }) => { const testDir = await createTmpDir('test-data'); // Create test files await fs.writeFile(path.join(testDir, 'test.bru'), testContent); // Use in test await page.getByLabel('Open Collection').click(); await page.getByText(testDir).click(); }); ``` ## Examples ### Example 1: Basic Collection Creation ```typescript import { test, expect } from '../../playwright'; test('should create a new collection', async ({ page, createTmpDir }) => { const testDir = await createTmpDir('new-collection'); await page.getByLabel('Create Collection').click(); await page.getByLabel('Name').fill('My Test Collection'); await page.getByLabel('Location').fill(testDir); await page.getByRole('button', { name: 'Create' }).click(); await expect(page.getByText('My Test Collection')).toBeVisible(); }); ``` ### Example 2: Request Creation and Execution ```typescript import { test, expect } from '../../playwright'; test('should create and execute HTTP request', async ({ page, createTmpDir }) => { const testDir = await createTmpDir('request-test'); // Create collection await page.getByLabel('Create Collection').click(); await page.getByLabel('Name').fill('Request Test'); await page.getByLabel('Location').fill(testDir); await page.getByRole('button', { name: 'Create' }).click(); // Create request await page.locator('#create-new-tab').getByRole('img').click(); await page.getByPlaceholder('Request Name').fill('Test Request'); await page.locator('#new-request-url .CodeMirror').click(); await page.locator('textarea').fill('http://localhost:8081/ping'); await page.getByRole('button', { name: 'Create' }).click(); // Execute request await page.locator('#send-request').getByRole('img').nth(2).click(); // Verify response await expect(page.getByRole('main')).toContainText('200 OK'); }); ``` ### Example 3: Environment Management ```typescript import { test, expect } from '../../playwright'; test('should create and use environment variables', async ({ page, createTmpDir }) => { const testDir = await createTmpDir('env-test'); // Setup collection await page.getByLabel('Create Collection').click(); await page.getByLabel('Name').fill('Environment Test'); await page.getByLabel('Location').fill(testDir); await page.getByRole('button', { name: 'Create' }).click(); // Create environment await page.getByRole('button', { name: 'Environments' }).click(); await page.getByRole('button', { name: 'Add Environment' }).click(); await page.getByLabel('Environment Name').fill('Development'); await page.getByRole('button', { name: 'Create' }).click(); // Add variable await page.getByRole('button', { name: 'Add Variable' }).click(); await page.getByLabel('Variable Name').fill('API_URL'); await page.getByLabel('Variable Value').fill('http://localhost:3000'); await page.getByRole('button', { name: 'Save' }).click(); await expect(page.getByText('API_URL')).toBeVisible(); }); ``` ## Troubleshooting ### Common Issues 1. **Electron App Not Starting** ```bash # Ensure dependencies are installed npm install # Try running the app manually first npm run dev:electron ``` 2. **Tests Timing Out** ```typescript // Increase timeout for specific test test('slow test', async ({ page }) => { test.setTimeout(60000); // 60 seconds // Test steps }); ``` 3. **Element Not Found** ```typescript // Wait for element to be present await page.waitForSelector('[data-testid="element"]'); // Or use more specific selectors await page.getByRole('button', { name: 'Exact Button Text' }).click(); ``` 4. **Flaky Tests** ```typescript // Use stable selectors await page.getByTestId('stable-id').click(); // Wait for state changes await page.waitForLoadState('networkidle'); ``` ### Debug Mode ```bash # Run with debug mode npx playwright test --debug # Run specific test in debug mode npx playwright test --debug e2e-tests/001-sanity-tests/001-home-screen.spec.ts ``` ### Trace Analysis ```bash # Run with trace recording npx playwright test --trace on # View trace in browser npx playwright show-trace test-results/trace-*.zip ``` ## Configuration The Playwright configuration is in `playwright.config.ts`: ```typescript export default defineConfig({ testDir: './e2e-tests', fullyParallel: false, forbidOnly: !!process.env.CI, retries: process.env.CI ? 1 : 0, workers: process.env.CI ? undefined : 1, projects: [ { name: 'Bruno Electron App' } ], webServer: [ { command: 'npm run dev:web', url: 'http://localhost:3000', reuseExistingServer: !process.env.CI }, { command: 'npm start --workspace=packages/bruno-tests', url: 'http://localhost:8081/ping', reuseExistingServer: !process.env.CI } ] }); ``` ## Additional Resources - [Playwright Documentation](https://playwright.dev/) - [Playwright Test API](https://playwright.dev/docs/api/class-test) - [Electron Testing with Playwright](https://playwright.dev/docs/api/class-electronapplication) - [Bruno Project Structure](../readme.md) --- For questions or issues with testing, please refer to the project's contributing guidelines or create an issue in the repository. ================================================ FILE: docs/publishing/publishing_bn.md ================================================ [English](../../publishing.md) ### ব্রুনোকে নতুন প্যাকেজ ম্যানেজারে প্রকাশ করা যদিও আমাদের কোড ওপেন সোর্স এবং সবার ব্যবহারের জন্য উপলব্ধ, তবে আমরা নতুন প্যাকেজ ম্যানেজারে প্রকাশনা বিবেচনা করার আগে আমাদের সাথে যোগাযোগ করার জন্য অনুরোধ করি। ব্রুনোর স্রষ্টা হিসাবে, আমি এই প্রকল্পের জন্য `Bruno` ট্রেডমার্ক ধারণ করি এবং এর বিতরণ পরিচালনা করতে চাই। যদি আপনি একটি নতুন প্যাকেজ ম্যানেজারে ব্রুনো দেখতে চান, দয়া করে একটি GitHub ইস্যু তুলুন। যদিও আমাদের বেশিরভাগ বৈশিষ্ট্য বিনামূল্যে এবং ওপেন সোর্স (যা REST এবং GraphQL API গুলিকে কভার করে), আমরা ওপেন-সোর্স নীতি এবং স্থায়িত্বের মধ্যে একটি সুসঙ্গত ভারসাম্য বজায় রাখার জন্য চেষ্টা করি - https://github.com/usebruno/bruno/discussions/269 ================================================ FILE: docs/publishing/publishing_cn.md ================================================ [English](../../publishing.md) ### 将 Bruno 发布到新的包管理器 虽然我们的代码是开源的,每个人都可以使用,但我们恳请您在考虑在新的包管理器上发布之前与我们联系。作为 Bruno 的创建者,我拥有这个项目的 Bruno 商标并希望管理其发行。如果您希望看到它使用新的包管理器,请提交一个 GitHub issue。 虽然我们的大部分功能都是免费与开源的 (涵盖 REST 和 GraphQL APIs) ,但我们努力在开源原则和可持续性之间取得和谐的平衡 - https://github.com/usebruno/bruno/discussions/269 ================================================ FILE: docs/publishing/publishing_de.md ================================================ [English](../../publishing.md) ### Veröffentlichung von Bruno über neue Paket-Manager Obwohl 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. Als Schöpfer von Bruno liegen alle Marktrechte von `Bruno` bei mir und ich möchte die volle Kontrolle über alle Verbreitungswege behalten. Falls Bruno über einen weiteren Paketmanager veröffentlicht werden soll, eröffne bitte ein GitHub-Issue. Während ein Großteil der Features kostenlos und Open Source ist (beinhaltet REST und GraphQL APIs), bemühen wir uns um ein harmonisches Gleichgewicht zwischen Open-Source-Prinzipien und Nachhaltigkeit - https://github.com/usebruno/bruno/discussions/269 ================================================ FILE: docs/publishing/publishing_fa.md ================================================ [English](../../publishing.md) ### انتشار Bruno در یک پکیج منیجر جدید اگرچه کد ما متن‌باز است و همه می‌توانند از آن استفاده کنند، لطفاً قبل از انتشار Bruno در مدیر بسته‌های جدید با ما تماس بگیرید. به عنوان سازنده Bruno، علامت تجاری `Bruno` را برای این پروژه دارم و مایلم توزیع آن را مدیریت کنم. اگر دوست دارید Bruno را در یک مدیر بسته جدید ببینید، لطفاً یک issue در گیت‌هاب ثبت کنید. اگرچه بیشتر قابلیت‌های ما رایگان و متن‌باز هستند (شامل REST و GraphQL Apis)، ما تلاش می‌کنیم بین اصول متن‌باز و توسعه پایدار تعادل مناسبی برقرار کنیم - https://github.com/usebruno/bruno/discussions/269 ================================================ FILE: docs/publishing/publishing_fr.md ================================================ [English](../../publishing.md) ### Publier Bruno dans un nouveau gestionnaire de paquets Bien 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. Bien 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 ================================================ FILE: docs/publishing/publishing_ja.md ================================================ [English](../../publishing.md) ### Bruno を新しいパッケージマネージャに公開する場合の注意 私たちのソースコードはオープンソースで誰でも使用できますが、新しいパッケージマネージャで公開を検討する前に、私たちにご連絡ください。私は Bruno の製作者として、このプロジェクト「Bruno」の商標を保有しており、その配布を管理したいと考えています。もし新しいパッケージマネージャで Bruno を使いたい場合は、GitHub の issue を立ててください。 私たちの機能の大部分が無料でオープンソース(REST や GraphQL の API も含む)ですが、 私たちはオープンソースの原則と長期的な維持の間でよいバランスをとれるように努力しています- https://github.com/usebruno/bruno/discussions/269 ================================================ FILE: docs/publishing/publishing_nl.md ================================================ [English](../../publishing.md) ### Bruno publiceren naar een nieuwe pakketbeheerder Hoewel 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. Hoewel 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 ================================================ FILE: docs/publishing/publishing_pl.md ================================================ [English](../../publishing.md) ### Publikowanie Bruno w nowym menedżerze pakietów Chociaż 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. Chociaż większość naszych funkcji jest darmowa i otwartoźródłowa (co obejmuje REST i GraphQL Apis), staramy się osiągnąć harmonijny balans między zasadami open-source a zrównoważonym rozwojem - https://github.com/usebruno/bruno/discussions/269 ================================================ FILE: docs/publishing/publishing_pt_br.md ================================================ [English](../../publishing.md) ### Publicando Bruno em um novo gerenciador de pacotes Embora 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. Embora 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 ================================================ FILE: docs/publishing/publishing_ro.md ================================================ [English](../../publishing.md) ### Publicarea lui Bruno la un gestionar de pachete nou Deș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 timp ce majoritatea funcțiilor noastre sunt gratuite și cu sursă deschisă (ceea ce acoperă API-uri REST și GraphQL), ne străduim să găsim un echilibru armonios între principiile de sursă deschisă și sustenabilitate - https://github.com/usebruno/bruno/discussions/269 ================================================ FILE: docs/publishing/publishing_tr.md ================================================ [English](../../publishing.md) ### Bruno'yu yeni bir paket yöneticisine yayınlama Kodumuz 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. Özelliklerimizin çoğu ücretsiz ve açık kaynak olsa da (REST ve GraphQL Apis'i kapsar), açık kaynak ilkeleri ile sürdürülebilirlik arasında uyumlu bir denge kurmaya çalışıyoruz - https://github.com/usebruno/bruno/discussions/269 ================================================ FILE: docs/publishing/publishing_zhtw.md ================================================ [English](../../publishing.md) ### 將 Bruno 發佈到新的套件管理器 雖然我們的程式碼是開源的並且可供所有人使用,但我們懇請您在考慮在新的套件管理器上發布之前與我們聯繫。作為 Bruno 的創建者,我擁有這個專案的 Bruno 商標並希望管理其發行。如果您希望看到 Bruno 使用新的套件管理器,請提出一個 GitHub issue。 雖然我們的大部分功能都是免費和開源(涵蓋 REST 和 GraphQL APIs),但我們努力在開源的原則和永續性之間,取得和諧的平衡 - https://github.com/usebruno/bruno/discussions/269 ================================================ FILE: docs/readme/readme_ar.md ================================================

### برونو - بيئة تطوير مفتوحة المصدر لاستكشاف واختبار واجهات برمجة التطبيقات (APIs). [![GitHub version](https://badge.fury.io/gh/usebruno%2Fbruno.svg)](https://badge.fury.io/gh/usebruno%2Fbruno) [![CI](https://github.com/usebruno/bruno/actions/workflows/tests.yml/badge.svg?branch=main)](https://github.com/usebruno/bruno/actions/workflows/tests.yml) [![Commit Activity](https://img.shields.io/github/commit-activity/m/usebruno/bruno)](https://github.com/usebruno/bruno/pulse) [![X](https://img.shields.io/twitter/follow/use_bruno?style=social&logo=x)](https://twitter.com/use_bruno) [![Website](https://img.shields.io/badge/Website-Visit-blue)](https://www.usebruno.com) [![Download](https://img.shields.io/badge/Download-Latest-brightgreen)](https://www.usebruno.com/downloads) [English](../../readme.md) | [Українська](./readme_ua.md) | [Русский](./readme_ru.md) | [Türkçe](./readme_tr.md) | [Deutsch](./readme_de.md) | [Français](./readme_fr.md) | [Português (BR)](./readme_pt_br.md) | [한국어](./readme_kr.md) | [বাংলা](./readme_bn.md) | [Español](./readme_es.md) | [Italiano](./readme_it.md) | [Română](./readme_ro.md) | [Polski](./readme_pl.md) | [简体中文](./readme_cn.md) | [正體中文](./readme_zhtw.md) | **العربية** | [日本語](./readme_ja.md) | [ქართული](./readme_ka.md) برونو هو عميل API جديد ومبتكر، يهدف إلى ثورة الحالة الحالية التي يمثلها برنامج Postman وأدوات مماثلة هناك. يقوم برونو بتخزين مجموعاتك مباشرة في مجلد على نظام الملفات الخاص بك. نحن نستخدم لغة ترميز النص العادية، Bru، لحفظ معلومات حول طلبات واجهة برمجة التطبيقات (API). يمكنك استخدام Git أو أي نظام تحكم في الإصدار الذي تفضله للتعاون على مجموعات API الخاصة بك. برونو هو خاص بالاستخدام دون اتصال بالإنترنت. ليس هناك خطط لإضافة مزامنة السحابة إلى برونو أبدًا. نحن نقدر خصوصية بياناتك ونعتقد أنه يجب أن تظل على جهازك. اقرأ رؤيتنا على المدى الطويل [هنا](https://github.com/usebruno/bruno/discussions/269) 📢 شاهد حديثنا الأخير في مؤتمر India FOSS 3.0 [هنا](https://www.youtube.com/watch?v=7bSMFpbcPiY) ![bruno](/assets/images/landing-2.png)

### التثبيت برونو متاح كتنزيل ثنائي [على موقعنا على الويب](https://www.usebruno.com/downloads) لأنظمة التشغيل Mac و Windows و Linux. يمكنك أيضًا تثبيت برونو عبر مديري الحزم مثل Homebrew و Chocolatey و Scoop و Snap و Flatpak و Apt. ```sh # على نظام Mac عبر Homebrew brew install bruno # على نظام Windows عبر Chocolatey choco install bruno # على نظام Windows عبر Scoop scoop bucket add extras scoop install bruno # على نظام Linux عبر Snap snap install bruno # على نظام Linux عبر Flatpak flatpak install com.usebruno.Bruno # على نظام Linux عبر Apt sudo mkdir -p /etc/apt/keyrings sudo apt update && sudo apt install gpg curl curl -fsSL "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x9FA6017ECABE0266" \ | gpg --dearmor \ | sudo tee /etc/apt/keyrings/bruno.gpg > /dev/null sudo chmod 644 /etc/apt/keyrings/bruno.gpg echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/bruno.gpg] http://debian.usebruno.com/ bruno stable" \ | sudo tee /etc/apt/sources.list.d/bruno.list sudo apt update && sudo apt install bruno ``` ### التشغيل عبر منصات متعددة 🖥️ ![bruno](/assets/images/run-anywhere.png)

### التعاون عبر Git 👩‍💻🧑‍💻 أو أي نظام تحكم في الإصدار الذي تفضله ![bruno](/assets/images/version-control.png)

### الروابط المهمة 📌 - [رؤيتنا على المدى الطويل](https://github.com/usebruno/bruno/discussions/269) - [خارطة الطريق](https://github.com/usebruno/bruno/discussions/384) - [التوثيق](https://docs.usebruno.com) - [Stack Overflow](https://stackoverflow.com/questions/tagged/bruno) - [الموقع الإلكتروني](https://www.usebruno.com) - [التسعير](https://www.usebruno.com/pricing) - [التنزيل](https://www.usebruno.com/downloads) - [Github Sponsors](https://github.com/sponsors/helloanoop). ### عروض 🎥 - [الشهادات](https://github.com/usebruno/bruno/discussions/343) - [مركز المعرفة](https://github.com/usebruno/bruno/discussions/386) - [Scriptmania](https://github.com/usebruno/bruno/discussions/385) ### الدعم ❤️ إذا كنت تحب برونو وترغب في دعم عملنا مفتوح المصدر، فكر في رعايتنا عبر [Github Sponsors](https://github.com/sponsors/helloanoop). ### شارك الشهادات 📣 إذا كان برونو قد ساعدك في العمل وفرقك، فلا تنسى مشاركة [شهاداتك في مناقشتنا على GitHub](https://github.com/usebruno/bruno/discussions/343) ### نشر إلى مديري الحزم الجديدة يرجى الرجوع [هنا](../../publishing.md) لمزيد من المعلومات. ### تواصل معنا 🌐 [𝕏 (تويتر)](https://twitter.com/use_bruno)
[الموقع الإلكتروني](https://www.usebruno.com)
[ديسكورد](https://discord.com/invite/KgcZUncpjq)
[لينكدإن](https://www.linkedin.com/company/usebruno) ### علامة تجارية **الاسم** `برونو` هو علامة تجارية تمتلكها [أنوب إم دي](https://www.helloanoop.com/) **الشعار** الشعار من [OpenMoji](https://openmoji.org/library/emoji-1F436/). الترخيص: CC [BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/) ### المساهمة 👩‍💻🧑‍💻 يسعدني أنك تتطلع لتحسين برونو. يرجى الاطلاع على [دليل المساهمة](../../contributing.md) حتى إذا لم تكن قادرًا على التساهم بشكل مباشر من خلال الشيفرة، فلا تتردد في الإبلاغ عن الأخطاء وطلب الميزات التي يجب تنفيذها لحل حالتك. ### الكتّاب
### الرخصة 📄 [MIT](../../license.md) ================================================ FILE: docs/readme/readme_bn.md ================================================
### ব্রুনো - API অন্বেষণ এবং পরীক্ষা করার জন্য ওপেনসোর্স IDE। [![GitHub version](https://badge.fury.io/gh/usebruno%2Fbruno.svg)](https://badge.fury.io/gh/usebruno%2Fbruno) [![CI](https://github.com/usebruno/bruno/actions/workflows/tests.yml/badge.svg?branch=main)](https://github.com/usebruno/bruno/actions/workflows/tests.yml) [![Commit Activity](https://img.shields.io/github/commit-activity/m/usebruno/bruno)](https://github.com/usebruno/bruno/pulse) [![X](https://img.shields.io/twitter/follow/use_bruno?style=social&logo=x)](https://twitter.com/use_bruno) [![Website](https://img.shields.io/badge/Website-Visit-blue)](https://www.usebruno.com) [![Download](https://img.shields.io/badge/Download-Latest-brightgreen)](https://www.usebruno.com/downloads) [English](../../readme.md) | [Українська](./readme_ua.md) | [Русский](./readme_ru.md) | [Türkçe](./readme_tr.md) | [Deutsch](./readme_de.md) | [Français](./readme_fr.md) | [Português (BR)](./readme_pt_br.md) | [한국어](./readme_kr.md) | **বাংলা** | [Español](./readme_es.md) | [Italiano](./readme_it.md) | [Română](./readme_ro.md) | [Polski](./readme_pl.md) | [简体中文](./readme_cn.md) | [正體中文](./readme_zhtw.md) | [العربية](./readme_ar.md) | [日本語](./readme_ja.md) | [ქართული](./readme_ka.md) ব্রুনো হল একটি নতুন এবং উদ্ভাবনী API ক্লায়েন্ট, যার লক্ষ্য পোস্টম্যান এবং অনুরূপ সরঞ্জাম দ্বারা প্রতিনিধিত্ব করা স্থিতাবস্থায় বিপ্লব ঘটানো। ব্রুনো আপনার সংগ্রহগুলি সরাসরি আপনার ফাইল সিস্টেমের একটি ফোল্ডারে সঞ্চয় করে। আমরা API অনুরোধ সম্পর্কে তথ্য সংরক্ষণ করতে একটি প্লেইন টেক্সট মার্কআপ ভাষা, ব্রু ব্যবহার করি। আপনি আপনার API সংগ্রহে সহযোগিতা করতে গিট বা আপনার পছন্দের যেকোনো সংস্করণ নিয়ন্ত্রণ ব্যবহার করতে পারেন। ব্রুনো শুধুমাত্র অফলাইন। ব্রুনোতে ক্লাউড-সিঙ্ক যোগ করার কোন পরিকল্পনা নেই, কখনও। আমরা আপনার ডেটা গোপনীয়তার মূল্য দিই এবং বিশ্বাস করি এটি আপনার ডিভাইসে থাকা উচিত। আমাদের দীর্ঘমেয়াদী দৃষ্টি পড়ুন। [এখানে ](https://github.com/usebruno/bruno/discussions/269) 📢 ইন্ডিয়া FOSS 3.0 সম্মেলনে আমাদের সাম্প্রতিক আলোচনা দেখুন [এখানে](https://www.youtube.com/watch?v=7bSMFpbcPiY) ![bruno](/assets/images/landing-2.png)

### স্থাপন ব্রুনো বাইনারি ডাউনলোড হিসাবে উপলব্ধ [আমাদের ওয়েবসাইটে](https://www.usebruno.com/downloads) ম্যাক, উইন্ডোজ এবং লিনাক্সের জন্য। আপনি Homebrew, Chocolatey, Snap এবং Apt এর মত প্যাকেজ ম্যানেজারদের মাধ্যমে ব্রুনো ইনস্টল করতে পারেন। ```sh # Homebrew এর মাধ্যমে Mac-এ brew install bruno # চকোলেটির মাধ্যমে উইন্ডোজে choco install bruno # স্ন্যাপ এর মাধ্যমে লিনাক্সে snap install bruno # Apt এর মাধ্যমে লিনাক্সে sudo mkdir -p /etc/apt/keyrings sudo apt update && sudo apt install gpg curl curl -fsSL "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x9FA6017ECABE0266" \ | gpg --dearmor \ | sudo tee /etc/apt/keyrings/bruno.gpg > /dev/null sudo chmod 644 /etc/apt/keyrings/bruno.gpg echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/bruno.gpg] http://debian.usebruno.com/ bruno stable" \ | sudo tee /etc/apt/sources.list.d/bruno.list sudo apt update && sudo apt install bruno ``` ### একাধিক প্ল্যাটফর্মে চালান 🖥️ ![bruno](/assets/images/run-anywhere.png)

### Git এর মাধ্যমে সহযোগিতা করুন 👩‍💻🧑‍💻 অথবা আপনার পছন্দের যেকোনো সংস্করণ নিয়ন্ত্রণ ব্যবস্থা ![bruno](/assets/images/version-control.png)

### গুরুত্বপূর্ণ লিংক 📌 - [আমাদের দীর্ঘমেয়াদী দৃষ্টি](https://github.com/usebruno/bruno/discussions/269) - [রোডম্যাপ](https://github.com/usebruno/bruno/discussions/384) - [ডকুমেন্টেশন](https://docs.usebruno.com) - [ওয়েবসাইট](https://www.usebruno.com) - [মূল্য](https://www.usebruno.com/pricing) - [ডাউনলোড করুন](https://www.usebruno.com/downloads) ### শোকেস 🎥 - [প্রশংসাপত্র](https://github.com/usebruno/bruno/discussions/343) - [নলেজ হাব](https://github.com/usebruno/bruno/discussions/386) - [স্ক্রিপ্টম্যানিয়া](https://github.com/usebruno/bruno/discussions/385) ### সমর্থন ❤️ উফ ! আপনি যদি প্রকল্পটি পছন্দ করেন তবে ⭐ বোতামটি টিপুন !! ### প্রশংসাপত্র শেয়ার করুন 📣 যদি ব্রুনো আপনাকে কর্মক্ষেত্রে এবং আপনার দলগুলিতে সাহায্য করে থাকে, অনুগ্রহ করে আপনার [আমাদের গিটহাব আলোচনায় প্রশংসাপত্রগুলি](https://github.com/usebruno/bruno/discussions/343) শেয়ার করতে ভুলবেন না ### নতুন প্যাকেজ পরিচালকদের কাছে প্রকাশ করা হচ্ছে আরও তথ্যের জন্য অনুগ্রহ করে [এখানে](../publishing/publishing_bn.md) দেখুন। ### অবদান 👩‍💻🧑‍💻 আমি খুশি যে আপনি ব্রুনোর উন্নতি করতে চাইছেন। অনুগ্রহ করে [অবদানকারী নির্দেশিকা](../contributing/contributing_bn.md) দেখুন আপনি কোডের মাধ্যমে অবদান রাখতে না পারলেও, অনুগ্রহ করে বাগ এবং বৈশিষ্ট্যের অনুরোধ ফাইল করতে দ্বিধা করবেন না যা আপনার ব্যবহারের ক্ষেত্রে সমাধান করার জন্য প্রয়োগ করা প্রয়োজন। ### লেখক
### সাথে থাকুন 🌐 [𝕏 (টুইটার)](https://twitter.com/use_bruno)
[ওয়েবসাইট](https://www.usebruno.com)
[ডিসকর্ড](https://discord.com/invite/KgcZUncpjq)
[লিঙ্কডইন](https://www.linkedin.com/company/usebruno) ### ট্রেডমার্ক **নাম** `Bruno` হল একটি ট্রেডমার্ক [Anoop M D](https://www.helloanoop.com/) **লোগো** লোগোটি [OpenMoji](https://openmoji.org/library/emoji-1F436/) থেকে নেওয়া হয়েছে। লাইসেন্স: CC [BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/) ### লাইসেন্স 📄 [MIT](../../license.md) ================================================ FILE: docs/readme/readme_cn.md ================================================
### Bruno - 开源 IDE,用于探索和测试 API。 [![GitHub version](https://badge.fury.io/gh/usebruno%2Fbruno.svg)](https://badge.fury.io/gh/usebruno%2Fbruno) [![CI](https://github.com/usebruno/bruno/actions/workflows/tests.yml/badge.svg?branch=main)](https://github.com/usebruno/bruno/actions/workflows/tests.yml) [![Commit Activity](https://img.shields.io/github/commit-activity/m/usebruno/bruno)](https://github.com/usebruno/bruno/pulse) [![X](https://img.shields.io/twitter/follow/use_bruno?style=social&logo=x)](https://twitter.com/use_bruno) [![网站](https://img.shields.io/badge/Website-Visit-blue)](https://www.usebruno.com) [![下载](https://img.shields.io/badge/Download-Latest-brightgreen)](https://www.usebruno.com/downloads) [English](../../readme.md) | [Українська](./readme_ua.md) | [Русский](./readme_ru.md) | [Türkçe](./readme_tr.md) | [Deutsch](./readme_de.md) | [Français](./readme_fr.md) | [Português (BR)](./readme_pt_br.md) | [한국어](./readme_kr.md) | [বাংলা](./readme_bn.md) | [Español](./readme_es.md) | [Italiano](./readme_it.md) | [Română](./readme_ro.md) | [Polski](./readme_pl.md) | **简体中文** | [正體中文](./readme_zhtw.md) | [العربية](./readme_ar.md) | [日本語](./readme_ja.md) | [ქართული](./readme_ka.md) Bruno 是一款全新且创新的 API 客户端,旨在颠覆 Postman 和其他类似工具。 Bruno 直接在您的电脑文件夹中存储您的 API 信息。我们使用纯文本标记语言 Bru 来保存有关 API 的信息。 您可以使用 Git 或您选择的任何版本控制系统来对您的 API 信息进行版本控制和协作。 Bruno 仅限离线使用。我们计划永不向 Bruno 添加云同步功能。我们重视您的数据隐私,并认为它应该留在您的设备上。阅读我们的长期愿景 [点击查看](https://github.com/usebruno/bruno/discussions/269) [下载 Bruno](https://www.usebruno.com/downloads) 📢 观看我们在印度 FOSS 3.0 会议上的最新演讲 [点击查看](https://www.youtube.com/watch?v=7bSMFpbcPiY) ![bruno](../../assets/images/landing-2.png)

## 商业版本 ✨ 我们的大多数功能都是免费且开源的。 我们致力于在 [开源与可持续性发展](https://github.com/usebruno/bruno/discussions/269) 之间取得和谐的平衡 欢迎使用我们的 [付费版本](https://www.usebruno.com/pricing) ,看看附加的功能是否对您或团队有所帮助!
## 目录 - [安装](#安装) - [特性](#特性) - [跨平台使用 🖥️](#跨平台使用-) - [通过Git协作 👩‍💻🧑‍💻](#通过git协作-) - [重要链接 📌](#重要链接-) - [展示 🎥](#展示-) - [分享评价 📣](#分享评价-) - [发布到新的包管理器](#发布到新的包管理器) - [联系方式 🌐](#联系方式-) - [商标](#商标) - [贡献 👩‍💻🧑‍💻](#贡献-) - [作者](#作者) - [许可证 📄](#许可证-) ## 安装 Bruno 可以在我们的 [网站上下载](https://www.usebruno.com/downloads) 适用于Mac、Windows 和 Linux 的可执行文件。 您也可以通过包管理器如 Homebrew、Chocolatey、Scoop、Snap 和 Apt 安装 Bruno。 ```sh # 在 Mac 电脑上用 Homebrew 安装 brew install bruno # 在 Windows 上用 Chocolatey 安装 choco install bruno # 在 Windows 上用 Scoop 安装 scoop bucket add extras scoop install bruno # 在 Windows 上用 winget 安装 winget install Bruno.Bruno # 在 Linux 上用 Snap 安装 snap install bruno # 在 Linux 上用 Flatpak 安装 flatpak install com.usebruno.Bruno # 在 Linux 上用 Apt 安装 sudo mkdir -p /etc/apt/keyrings sudo apt update && sudo apt install gpg curl curl -fsSL "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x9FA6017ECABE0266" \ | gpg --dearmor \ | sudo tee /etc/apt/keyrings/bruno.gpg > /dev/null sudo chmod 644 /etc/apt/keyrings/bruno.gpg echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/bruno.gpg] http://debian.usebruno.com/ bruno stable" \ | sudo tee /etc/apt/sources.list.d/bruno.list sudo apt update && sudo apt install bruno ``` ## 特性 ### 跨平台使用 🖥️ ![bruno](../../assets/images/run-anywhere.png)

### 通过Git协作 👩‍💻🧑‍💻 或者任何您选择的版本控制系统 ![bruno](../../assets/images/version-control.png)

## 重要链接 📌 - [我们的愿景](https://github.com/usebruno/bruno/discussions/269) - [路线图](https://www.usebruno.com/roadmap) - [文档](https://docs.usebruno.com) - [Stack Overflow](https://stackoverflow.com/questions/tagged/bruno) - [网站](https://www.usebruno.com) - [价格](https://www.usebruno.com/pricing) - [下载](https://www.usebruno.com/downloads) ## 展示 🎥 - [Testimonials](https://github.com/usebruno/bruno/discussions/343) - [Knowledge Hub](https://github.com/usebruno/bruno/discussions/386) - [Scriptmania](https://github.com/usebruno/bruno/discussions/385) ## 分享评价 📣 如果 Bruno 在您的工作和团队中帮助了您,请不要忘记在我们的 GitHub 讨论上分享您的 [评价](https://github.com/usebruno/bruno/discussions/343) ## 发布到新的包管理器 如需了解更多信息,请参见 [此处](../publishing/publishing_cn.md) 。 ## 联系方式 🌐 [𝕏 (Twitter)](https://twitter.com/use_bruno)
[Website](https://www.usebruno.com)
[Discord](https://discord.com/invite/KgcZUncpjq)
[LinkedIn](https://www.linkedin.com/company/usebruno) ## 商标 **名称** `Bruno` 是由 [Anoop M D](https://www.helloanoop.com/) 持有的商标。 **Logo** Logo 源自 [OpenMoji](https://openmoji.org/library/emoji-1F436/). License: CC [BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/) ## 贡献 👩‍💻🧑‍💻 很高兴您希望改进 bruno。请查看 [贡献指南](../contributing/contributing_cn.md)。 即使您无法通过代码做出贡献,我们仍然欢迎您提出 BUG 和新的功能需求。 ## 作者
## 许可证 📄 [MIT](../../license.md) ================================================ FILE: docs/readme/readme_de.md ================================================
### Bruno - Opensource IDE zum Erkunden und Testen von APIs. [![GitHub version](https://badge.fury.io/gh/usebruno%2Fbruno.svg)](https://badge.fury.io/gh/usebruno%2Fbruno) [![CI](https://github.com/usebruno/bruno/actions/workflows/tests.yml/badge.svg?branch=main)](https://github.com/usebruno/bruno/actions/workflows/tests.yml) [![Commit Activity](https://img.shields.io/github/commit-activity/m/usebruno/bruno)](https://github.com/usebruno/bruno/pulse) [![X](https://img.shields.io/twitter/follow/use_bruno?style=social&logo=x)](https://twitter.com/use_bruno) [![Website](https://img.shields.io/badge/Website-Visit-blue)](https://www.usebruno.com) [![Download](https://img.shields.io/badge/Download-Latest-brightgreen)](https://www.usebruno.com/downloads) [English](../../readme.md) | [Українська](./readme_ua.md) | [Русский](./readme_ru.md) | [Türkçe](./readme_tr.md) | **Deutsch** | [Français](./readme_fr.md) | [Português (BR)](./readme_pt_br.md) | [한국어](./readme_kr.md) | [বাংলা](./readme_bn.md) | [Español](./readme_es.md) | [Italiano](./readme_it.md) | [Română](./readme_ro.md) | [Polski](./readme_pl.md) | [简体中文](./readme_cn.md) | [正體中文](./readme_zhtw.md) | [العربية](./readme_ar.md) | [日本語](./readme_ja.md) | [ქართული](./readme_ka.md) Bruno ist ein neuer und innovativer API-Client, der den Status Quo von Postman und ähnlichen Tools revolutionieren soll. Bruno speichert deine Sammlungen direkt in einem Ordner in deinem Dateisystem. Wir verwenden eine einfache Textauszeichnungssprache - Bru - um Informationen über API-Anfragen zu speichern. Du kannst Git oder eine andere Versionskontrolle deiner Wahl verwenden, um gemeinsam mit anderen an deinen API-Sammlungen zu arbeiten. Bruno 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). [Download Bruno](https://www.usebruno.com/downloads) 📢 Sieh Dir unseren Vortrag auf der India FOSS 3.0 Conference [hier](https://www.youtube.com/watch?v=7bSMFpbcPiY) an. ![bruno](/assets/images/landing-2.png)

### Installation Bruno ist als Download [auf unserer Website](https://www.usebruno.com/downloads) für Mac, Windows und Linux verfügbar. Du kannst Bruno auch über Paketmanager wie Homebrew, Chocolatey, Scoop, Snap, Flatpak und Apt installieren. ```sh # Auf Mac via Homebrew brew install bruno # Auf Windows via Chocolatey choco install bruno # Auf Windows via Scoop scoop bucket add extras scoop install bruno # Auf Windows via winget winget install Bruno.Bruno # Auf Linux via Snap snap install bruno # Auf Linux via Flatpak flatpak install com.usebruno.Bruno # Auf Linux via Apt sudo mkdir -p /etc/apt/keyrings sudo apt update && sudo apt install gpg curl curl -fsSL "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x9FA6017ECABE0266" \ | gpg --dearmor \ | sudo tee /etc/apt/keyrings/bruno.gpg > /dev/null sudo chmod 644 /etc/apt/keyrings/bruno.gpg echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/bruno.gpg] http://debian.usebruno.com/ bruno stable" \ | sudo tee /etc/apt/sources.list.d/bruno.list sudo apt update && sudo apt install bruno ``` ### Einsatz auf verschiedensten Plattformen 🖥️ ![bruno](/assets/images/run-anywhere.png)

### Zusammenarbeit mit Git 👩‍💻🧑‍💻 Oder einer Versionskontrolle deiner Wahl ![bruno](/assets/images/version-control.png)

### Sponsoren #### Gold Sponsoren #### Silber Sponsoren ### Wichtige Links 📌 - [Unsere Langzeit-Vision](https://github.com/usebruno/bruno/discussions/269) - [Roadmap](https://github.com/usebruno/bruno/discussions/384) - [Dokumentation](https://docs.usebruno.com) - [Webseite](https://www.usebruno.com) - [Preise](https://www.usebruno.com/pricing) - [Download](https://www.usebruno.com/downloads) ### Showcase 🎥 - [Erfahrungsberichte](https://github.com/usebruno/bruno/discussions/343) - [Wissenswertes](https://github.com/usebruno/bruno/discussions/386) - [Scriptmania](https://github.com/usebruno/bruno/discussions/385) ### Unterstützung ❤️ Wuff! Wenn du dieses Projekt magst, klick auf den ⭐ Button !! ### Teile Erfahrungsberichte 📣 Wenn 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. ### Bereitstellung in neuen Paket-Managern Mehr Informationen findest du [hier](../publishing/publishing_de.md). ### Mitmachen 👩‍💻🧑‍💻 Ich freue mich, dass du Bruno verbessern willst. Bitte schau dir den [Leitfaden zum Mitmachen](../contributing/contributing_de.md) an. Auch 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. ### Autoren
### In Verbindung bleiben 🌐 [Twitter](https://twitter.com/use_bruno)
[Webseite](https://www.usebruno.com)
[Discord](https://discord.com/invite/KgcZUncpjq)
[LinkedIn](https://www.linkedin.com/company/usebruno) ### Markenzeichen **Name** `Bruno` ist ein Markenzeichen von [Anoop M D](https://www.helloanoop.com/) **Logo** Das Logo stammt von [OpenMoji](https://openmoji.org/library/emoji-1F436/). Lizenz: CC [BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/) ### Lizenz 📄 [MIT](../../license.md) ================================================ FILE: docs/readme/readme_es.md ================================================
### Bruno - IDE de código abierto para explorar y probar APIs. [![Versión en Github](https://badge.fury.io/gh/usebruno%2Fbruno.svg)](https://badge.fury.io/gh/usebruno%2Fbruno) [![CI](https://github.com/usebruno/bruno/actions/workflows/tests.yml/badge.svg?branch=main)](https://github.com/usebruno/bruno/actions/workflows/tests.yml) [![Actividad de Commits](https://img.shields.io/github/commit-activity/m/usebruno/bruno)](https://github.com/usebruno/bruno/pulse) [![X](https://img.shields.io/twitter/follow/use_bruno?style=social&logo=x)](https://twitter.com/use_bruno) [![Sitio Web](https://img.shields.io/badge/Website-Visit-blue)](https://www.usebruno.com) [![Descargas](https://img.shields.io/badge/Download-Latest-brightgreen)](https://www.usebruno.com/downloads) [English](../../readme.md) | [Українська](./readme_ua.md) | [Русский](./readme_ru.md) | [Türkçe](./readme_tr.md) | [Deutsch](./readme_de.md) | [Français](./readme_fr.md) | [Português (BR)](./readme_pt_br.md) | [한국어](./readme_kr.md) | [বাংলা](./readme_bn.md) | **Español** | [Italiano](./readme_it.md) | [Română](./readme_ro.md) | [Polski](./readme_pl.md) | [简体中文](./readme_cn.md) | [正體中文](./readme_zhtw.md) | [العربية](./readme_ar.md) | [日本語](./readme_ja.md) | [ქართული](./readme_ka.md) Bruno es un cliente de APIs nuevo e innovador, creado con el objetivo de revolucionar el panorama actual representado por Postman y otras herramientas similares. Bruno 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. Puedes usar git o cualquier otro sistema de control de versiones que prefieras para colaborar en tus colecciones. Bruno 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). [Descarga Bruno](https://www.usebruno.com/downloads). 📢 Mira nuestra charla en la conferencia India FOSS 3.0 [aquí](https://www.youtube.com/watch?v=7bSMFpbcPiY). ![bruno](/assets/images/landing-2.png)

### Instalación Bruno está disponible para su descarga [en nuestro sitio web](https://www.usebruno.com/downloads) para Mac, Windows y Linux. También puedes instalar Bruno mediante package managers como Homebrew, Chocolatey, Scoop, Flatpak y Apt. ```sh # En Mac con Homebrew brew install bruno # En Windows con Chocolatey choco install bruno # En Windows con Scoop scoop bucket add extras scoop install bruno # En Linux con Snap snap install bruno # En Linux con Flatpak flatpak install com.usebruno.Bruno # En Linux con Apt sudo mkdir -p /etc/apt/keyrings sudo apt update && sudo apt install gpg curl curl -fsSL "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x9FA6017ECABE0266" \ | gpg --dearmor \ | sudo tee /etc/apt/keyrings/bruno.gpg > /dev/null sudo chmod 644 /etc/apt/keyrings/bruno.gpg echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/bruno.gpg] http://debian.usebruno.com/ bruno stable" \ | sudo tee /etc/apt/sources.list.d/bruno.list sudo apt update && sudo apt install bruno ``` ### Ejecútalo en múltiples plataformas 🖥️ ![bruno](/assets/images/run-anywhere.png)

### Colabora vía Git 👩‍💻🧑‍💻 O cualquier otro sistema de control de versiones que prefieras ![bruno](/assets/images/version-control.png)

### Enlaces importantes 📌 - [Nuestra Visión a Largo Plazo](https://github.com/usebruno/bruno/discussions/269) - [Hoja de Ruta](https://github.com/usebruno/bruno/discussions/384) - [Documentación](https://docs.usebruno.com) - [Sitio Web](https://www.usebruno.com) - [Precios](https://www.usebruno.com/pricing) - [Descargas](https://www.usebruno.com/downloads) ### Casos de uso 🎥 - [Testimonios](https://github.com/usebruno/bruno/discussions/343) - [Centro de Conocimiento](https://github.com/usebruno/bruno/discussions/386) - [Scripts de la Comunidad](https://github.com/usebruno/bruno/discussions/385) ### Apoya el proyecto ❤️ ¡Guau! Si te gusta el proyecto, ¡dale al botón de ⭐! ### Comparte tus testimonios 📣 Si 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) ### Publicar en nuevos gestores de paquetes Por favor, consulta [aquí](../../publishing.md) para más información. ### Contribuye 👩‍💻🧑‍💻 Estamos 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. Incluso si no puedes contribuir con código, no dudes en reportar errores y solicitar nuevas funcionalidades que necesites para resolver tu caso de uso. ### Colaboradores
### Mantente en contacto 🌐 [X](https://twitter.com/use_bruno)
[Sitio Web](https://www.usebruno.com)
[Discord](https://discord.com/invite/KgcZUncpjq)
[LinkedIn](https://www.linkedin.com/company/usebruno) ### Marca **Nombre** `Bruno` es una marca propiedad de [Anoop M D](https://www.helloanoop.com/). **Logo** El 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/) ### Licencia 📄 [MIT](../../license.md) ================================================ FILE: docs/readme/readme_fa.md ================================================
### برونو یا Bruno - محیط توسعه متن باز برای تست و توسعه API ها [![GitHub version](https://badge.fury.io/gh/usebruno%2Fbruno.svg)](https://badge.fury.io/gh/usebruno%2Fbruno) [![CI](https://github.com/usebruno/bruno/actions/workflows/tests.yml/badge.svg?branch=main)](https://github.com/usebruno/bruno/actions/workflows/tests.yml) [![Commit Activity](https://img.shields.io/github/commit-activity/m/usebruno/bruno)](https://github.com/usebruno/bruno/pulse) [![X](https://img.shields.io/twitter/follow/use_bruno?style=social&logo=x)](https://twitter.com/use_bruno) [![Website](https://img.shields.io/badge/Website-Visit-blue)](https://www.usebruno.com) [![Download](https://img.shields.io/badge/Download-Latest-brightgreen)](https://www.usebruno.com/downloads) [English](../../readme.md) | [Українська](./readme_ua.md) | [Русский](./readme_ru.md) | [Türkçe](./readme_tr.md) | [Deutsch](./readme_de.md) | [Français](./readme_fr.md) | [Português (BR)](./readme_pt_br.md) | [한국어](./readme_kr.md) | [বাংলা](./readme_bn.md) | [Español](./readme_es.md) | **فارسی** | [Română](./readme_ro.md) | [Polski](./readme_pl.md) | [简体中文](./readme_cn.md) | [正體中文](./readme_zhtw.md) | [العربية](./readme_ar.md) | [日本語](./readme_ja.md) | [ქართული](./readme_ka.md) برونو یک کلاینت API جدید و نوآورانه است که هدفش تغییر وضعیت فعلی ابزارهایی مانند Postman و سایر ابزارهای مشابه است. برونو مجموعه‌های شما را مستقیماً در یک پوشه روی فایل‌سیستم شما ذخیره می‌کند. ما از یک زبان نشانه‌گذاری ساده به نام Bru برای ذخیره اطلاعات درخواست‌های API استفاده می‌کنیم. شما می‌توانید برای همکاری روی مجموعه‌های API خود، از Git یا هر سیستم کنترل نسخه دلخواهتان استفاده کنید. برونو فقط به صورت آفلاین کار می‌کند. هیچ برنامه‌ای برای اضافه کردن همگام‌سازی ابری به برونو در آینده وجود ندارد. ما به حریم خصوصی داده‌های شما اهمیت می‌دهیم و معتقدیم که باید روی دستگاه خودتان باقی بمانند. می‌توانید چشم‌انداز بلندمدت ما را مطالعه کنید. [اینجا (به انگلیسی)](https://github.com/usebruno/bruno/discussions/269) 📢 جدیدترین ارائه ما را در کنفرانس India FOSS 3.0 تماشا کنید. [اینجا](https://www.youtube.com/watch?v=7bSMFpbcPiY) ![bruno](/assets/images/landing-2.png)

### نصب برونو به صورت یک فایل باینری برای دانلود در دسترس است. [بر روی وبسایت ما](https://www.usebruno.com/downloads) برای مک لینکوس و ویندوز. همچنین می‌توانید برونو را از طریق مدیر بسته‌هایی مانند Homebrew، Chocolatey، Snap و Apt نصب کنید. ```sh # بر روی مک از طریق brew brew install bruno # بر روی ویندوز از طریق Chocolatey choco install bruno # بر روی لینوکس از طریق Snap snap install bruno # بر روی لینوکس از طریق Apt sudo mkdir -p /etc/apt/keyrings sudo apt update && sudo apt install gpg curl curl -fsSL "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x9FA6017ECABE0266" \ | gpg --dearmor \ | sudo tee /etc/apt/keyrings/bruno.gpg > /dev/null sudo chmod 644 /etc/apt/keyrings/bruno.gpg echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/bruno.gpg] http://debian.usebruno.com/ bruno stable" \ | sudo tee /etc/apt/sources.list.d/bruno.list sudo apt update && sudo apt install bruno ``` ### روی پلتفرم‌های مختلف کار می‌کند 🖥️ ![bruno](/assets/images/run-anywhere.png)

### همکاری از طریق گیت 👩‍💻🧑‍💻 یا هر سیستم کنترل نسخه‌ای که ترجیح می‌دهید ![bruno](/assets/images/version-control.png)

### لینک‌های مهم 📌 - [آخرین نسخه پایدار ما](https://github.com/usebruno/bruno/discussions/269) - [نقشه راه](https://github.com/usebruno/bruno/discussions/384) - [مستندات](https://docs.usebruno.com) - [وبسایت](https://www.usebruno.com) - [اشتراک ها](https://www.usebruno.com/pricing) - [دانلود](https://www.usebruno.com/downloads) ### ویدیوها 🎥 - [تجربه ها](https://github.com/usebruno/bruno/discussions/343) - [مرکز دانش](https://github.com/usebruno/bruno/discussions/386) - [اسکریپ مانیا](https://github.com/usebruno/bruno/discussions/385) ### حمایت ❤️ جوون! اگر این پروژه را دوست دارید، روی دکمه ⭐ کلیک کنید! ### تجربه‌های به اشتراک گذاشته‌شده 📣 اگر برونو به شما یا تیمتان کمک کرده است، لطفاً فراموش نکنید تجربه‌های خود را به اشتراک بگذارید. [تجربه‌های خود را در بحث گیت‌هاب ما به اشتراک بگذارید](https://github.com/usebruno/bruno/discussions/343). ### انتشار برونو در یک پکیچ منیجر جدید لطفا چک بکنید [اینجارو](../../publishing.md) برای اطلاعات بیشتر. ### مشارکت 👩‍💻🧑‍💻 خوشحالم که می‌خواهید برونو را بهتر کنید. لطفا [راهنمای مشارکت را بررسی کنید](../contributing/contributing_fa.md). حتی اگر نمی‌توانید از طریق کدنویسی مشارکت کنید، در گزارش باگ‌ها و درخواست قابلیت‌های جدید که به حل نیازهای شما کمک می‌کند تردید نکنید. ### نویسنده ها
### در ارتباط باشید 🌐 [𝕏 (تویتر)](https://twitter.com/use_bruno)
[وبسایت](https://www.usebruno.com)
[دیسکورد](https://discord.com/invite/KgcZUncpjq)
[لینکدین](https://www.linkedin.com/company/usebruno) ### برند **نام** به فارسی برونو - `Bruno` یک علامت تجاری ثبت‌شده متعلق به [Anoop M D](https://www.helloanoop.com/) **لوگو** لوگو توسط [OpenMoji](https://openmoji.org/library/emoji-1F436/) ساخته شده است. مجوز: CC [BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/) ### مجوز 📄 [MIT](../../license.md) ================================================ FILE: docs/readme/readme_fr.md ================================================
### Bruno - IDE Opensource pour explorer et tester des APIs. [![GitHub version](https://badge.fury.io/gh/usebruno%2Fbruno.svg)](https://badge.fury.io/gh/usebruno%2Fbruno) [![CI](https://github.com/usebruno/bruno/actions/workflows/tests.yml/badge.svg?branch=main)](https://github.com/usebruno/bruno/actions/workflows/tests.yml) [![Commit Activity](https://img.shields.io/github/commit-activity/m/usebruno/bruno)](https://github.com/usebruno/bruno/pulse) [![X](https://img.shields.io/twitter/follow/use_bruno?style=social&logo=x)](https://twitter.com/use_bruno) [![Website](https://img.shields.io/badge/Website-Visit-blue)](https://www.usebruno.com) [![Download](https://img.shields.io/badge/Download-Latest-brightgreen)](https://www.usebruno.com/downloads) [English](../../readme.md) | [Українська](./readme_ua.md) | [Русский](./readme_ru.md) | [Türkçe](./readme_tr.md) | [Deutsch](./readme_de.md) | **Français** | [Português (BR)](./readme_pt_br.md) | [한국어](./readme_kr.md) | [বাংলা](./readme_bn.md) | [Español](./readme_es.md) | [Italiano](./readme_it.md) | [Română](./readme_ro.md) | [Polski](./readme_pl.md) | [简体中文](./readme_cn.md) | [正體中文](./readme_zhtw.md) | [العربية](./readme_ar.md) | [日本語](./readme_ja.md) | [ქართული](./readme_ka.md) Bruno est un nouveau client API, innovant, qui a pour but de révolutionner le _statu quo_ que représentent Postman et les autres outils. Bruno 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. Vous pouvez utiliser git ou tout autre gestionnaire de version pour travailler de manière collaborative sur vos collections d'APIs. Bruno 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). 📢 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) ![bruno](/assets/images/landing-2.png)

### Installation Bruno est disponible au téléchargement [sur notre site web](https://www.usebruno.com/downloads), pour Mac, Windows et Linux. Vous pouvez aussi installer Bruno via un gestionnaire de paquets, comme Homebrew, Chocolatey, Scoop, Snap et Apt. ```sh # Mac via Homebrew brew install bruno # Windows via Chocolatey choco install bruno # Windows via Scoop scoop bucket add extras scoop install bruno # Linux via Snap snap install bruno # Linux via Apt sudo mkdir -p /etc/apt/keyrings sudo apt update && sudo apt install gpg curl curl -fsSL "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x9FA6017ECABE0266" \ | gpg --dearmor \ | sudo tee /etc/apt/keyrings/bruno.gpg > /dev/null sudo chmod 644 /etc/apt/keyrings/bruno.gpg echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/bruno.gpg] http://debian.usebruno.com/ bruno stable" \ | sudo tee /etc/apt/sources.list.d/bruno.list sudo apt update && sudo apt install bruno ``` ### Fonctionne sur de multiples plateformes 🖥️ ![bruno](/assets/images/run-anywhere.png)

### Collaborer via Git 👩‍💻🧑‍💻 Ou n'importe quel système de gestion de sources ![bruno](/assets/images/version-control.png)

### Liens importants 📌 - [Notre vision à long terme (en anglais)](https://github.com/usebruno/bruno/discussions/269) - [Roadmap](https://github.com/usebruno/bruno/discussions/384) - [Documentation](https://docs.usebruno.com) - [Site web](https://www.usebruno.com) - [Prix](https://www.usebruno.com/pricing) - [Téléchargement](https://www.usebruno.com/downloads) - [Sponsors GitHub](https://github.com/sponsors/helloanoop) ### Showcase 🎥 - [Témoignages](https://github.com/usebruno/bruno/discussions/343) - [Centre de connaissance](https://github.com/usebruno/bruno/discussions/386) - [Scriptmania](https://github.com/usebruno/bruno/discussions/385) ### Soutien ❤️ Si 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). ### Partage de témoignages 📣 Si 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) ### Publier Bruno sur un nouveau gestionnaire de paquets Veuillez regarder [ici](../publishing/publishing_fr.md) pour plus d'information. ### Contribuer 👩‍💻🧑‍💻 Je suis heureux de voir que vous cherchez à améliorer Bruno. Merci de consulter le [guide de contribution](../contributing/contributing_fr.md) Mê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 ! ### Auteurs
### Restons en contact 🌐 [Twitter](https://twitter.com/use_bruno)
[Website](https://www.usebruno.com)
[Discord](https://discord.com/invite/KgcZUncpjq)
[LinkedIn](https://www.linkedin.com/company/usebruno) ### Marque **Nom** `Bruno` est une marque appartenant à [Anoop M D](https://www.helloanoop.com/) **Logo** Le logo est issu de [OpenMoji](https://openmoji.org/library/emoji-1F436/). Licence : CC [BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/) ### Licence 📄 [MIT](../../license.md) ================================================ FILE: docs/readme/readme_hi.md ================================================
### ब्रूनो - API इंटरफेस (API) का अन्वेषण और परीक्षण करने के लिए एक ओपन-सोर्स विकास वातावरण। [![GitHub संस्करण](https://badge.fury.io/gh/usebruno%2Fbruno.svg)](https://badge.fury.io/gh/usebruno%2Fbruno) [![CI](https://github.com/usebruno/bruno/actions/workflows/tests.yml/badge.svg?branch=main)](https://github.com/usebruno/bruno/actions/workflows/tests.yml) [![कमिट गतिविधि](https://img.shields.io/github/commit-activity/m/usebruno/bruno)](https://github.com/usebruno/bruno/pulse) [![X](https://img.shields.io/twitter/follow/use_bruno?style=social&logo=x)](https://twitter.com/use_bruno) [![वेबसाइट](https://img.shields.io/badge/Website-Visit-blue)](https://www.usebruno.com) [![डाउनलोड](https://img.shields.io/badge/Download-Latest-brightgreen)](https://www.usebruno.com/downloads) [English](../../readme.md) | [Українська](./readme_ua.md) | [Русский](./readme_ru.md) | [Türkçe](./readme_tr.md) | [Deutsch](./readme_de.md) | [Français](./readme_fr.md) | [Português (BR)](./readme_pt_br.md) | [한국어](./readme_kr.md) | [বাংলা](./readme_bn.md) | [Español](./readme_es.md) | [Italiano](./readme_it.md) | [Română](./readme_ro.md) | [Polski](./readme_pl.md) | [简体中文](./readme_cn.md) | [正體中文](./readme_zhtw.md) | [العربية](./readme_ar.md) | [日本語](./readme_ja.md) | [ქართული](./readme_ka.md) | **हिन्दी** ब्रूनो एक नया और अभिनव API क्लाइंट है, जिसका उद्देश्य Postman और अन्य समान उपकरणों द्वारा प्रस्तुत स्थिति को बदलना है। ब्रूनो आपकी कलेक्शनों को सीधे आपकी फाइल सिस्टम के एक फ़ोल्डर में संग्रहीत करता है। हम API अनुरोधों के बारे में जानकारी सहेजने के लिए एक सामान्य टेक्स्ट मार्कअप भाषा, Bru, का उपयोग करते हैं। आप अपनी API कलेक्शनों पर सहयोग करने के लिए Git या अपनी पसंद के किसी भी संस्करण नियंत्रण प्रणाली का उपयोग कर सकते हैं। ब्रूनो केवल ऑफ़लाइन उपयोग के लिए है। ब्रूनो में कभी भी क्लाउड-सिंक जोड़ने की कोई योजना नहीं है। हम आपके डेटा की गोपनीयता को महत्व देते हैं और मानते हैं कि इसे आपके डिवाइस पर ही रहना चाहिए। हमारी दीर्घकालिक दृष्टि [यहाँ](https://github.com/usebruno/bruno/discussions/269) पढ़ें। 📢 हमारे हालिया India FOSS 3.0 सम्मेलन में हमारे वार्तालाप को [यहाँ](https://www.youtube.com/watch?v=7bSMFpbcPiY) देखें। ![bruno](/assets/images/landing-2.png)

### गोल्डन संस्करण ✨ हमारी अधिकांश सुविधाएँ मुफ्त और ओपन-सोर्स हैं। हम [पारदर्शिता और स्थिरता के सिद्धांतों](https://github.com/usebruno/bruno/discussions/269) के बीच एक सामंजस्यपूर्ण संतुलन प्राप्त करने का प्रयास करते हैं। [गोल्डन संस्करण](https://www.usebruno.com/pricing) के लिए खरीदारी जल्द ही $9 की कीमत पर उपलब्ध होगी!
[यहाँ सदस्यता लें](https://usebruno.ck.page/4c65576bd4) ताकि आपको लॉन्च पर सूचनाएं मिलें। ### स्थापना ब्रूनो Mac, Windows और Linux के लिए हमारे [वेबसाइट](https://www.usebruno.com/downloads) पर एक बाइनरी डाउनलोड के रूप में उपलब्ध है। आप ब्रूनो को Homebrew, Chocolatey, Scoop, Snap, Flatpak और Apt जैसे पैकेज प्रबंधकों के माध्यम से भी स्थापित कर सकते हैं। ```sh # Mac पर Homebrew के माध्यम से brew install bruno # Windows पर Chocolatey के माध्यम से choco install bruno # Windows पर Scoop के माध्यम से scoop bucket add extras scoop install bruno # Linux पर Snap के माध्यम से snap install bruno # Linux पर Flatpak के माध्यम से flatpak install com.usebruno.Bruno # Linux पर Apt के माध्यम से sudo mkdir -p /etc/apt/keyrings sudo apt update && sudo apt install gpg curl curl -fsSL "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x9FA6017ECABE0266" \ | gpg --dearmor \ | sudo tee /etc/apt/keyrings/bruno.gpg > /dev/null sudo chmod 644 /etc/apt/keyrings/bruno.gpg echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/bruno.gpg] http://debian.usebruno.com/ bruno stable" \ | sudo tee /etc/apt/sources.list.d/bruno.list sudo apt update && sudo apt install bruno कई प्लेटफार्मों पर चलाएं 🖥️

Git के माध्यम से सहयोग करें 👩‍💻🧑‍💻 या अपनी पसंद के किसी भी संस्करण नियंत्रण प्रणाली का उपयोग करें

महत्वपूर्ण लिंक 📌 हमारी दीर्घकालिक दृष्टि रोडमैप प्रलेखन Stack Overflow वेबसाइट मूल्य निर्धारण डाउनलोड GitHub प्रायोजक प्रस्तुतियाँ 🎥 प्रशंसापत्र ज्ञान केंद्र Scriptmania समर्थन ❤️ यदि आप ब्रूनो को पसंद करते हैं और हमारे ओपन-सोर्स कार्य का समर्थन करना चाहते हैं, तो कृपया GitHub प्रायोजक के माध्यम से हमें प्रायोजित करने पर विचार करें। प्रशंसापत्र साझा करें 📣 यदि ब्रूनो ने आपके और आपकी टीमों के लिए काम में मदद की है, तो कृपया हमारे GitHub चर्चा में अपने प्रशंसापत्र साझा करना न भूलें नए पैकेज प्रबंधकों में प्रकाशित करना अधिक जानकारी के लिए कृपया यहाँ देखें। हमसे संपर्क करें 🌐 𝕏 (ट्विटर)
वेबसाइट
डिस्कॉर्ड
लिंक्डइन ट्रेडमार्क नाम ब्रूनो एक ट्रेडमार्क है जो अनूप एम डी के स्वामित्व में है। लोगो लोगो OpenMoji से लिया गया है। लाइसेंस: CC BY-SA 4.0 योगदान 👩‍💻🧑‍💻 हमें खुशी है कि आप ब्रूनो को बेहतर बनाने में रुचि रखते हैं। कृपया योगदान गाइड देखें। यदि आप सीधे कोड के माध्यम से योगदान नहीं कर सकते, तो भी कृपया बग्स की रिपोर्ट करने और उन सुविधाओं का अनुरोध करने में संकोच न करें जिन्हें आपकी स्थिति को हल करने के लिए लागू किया जाना चाहिए। लेखक
लाइसेंस 📄 MIT ================================================ FILE: docs/readme/readme_it.md ================================================
### Bruno - Opensource IDE per esplorare e testare gli APIs. [![GitHub version](https://badge.fury.io/gh/usebruno%2Fbruno.svg)](https://badge.fury.io/gh/usebruno%2Fbruno) [![CI](https://github.com/usebruno/bruno/actions/workflows/tests.yml/badge.svg?branch=main)](https://github.com/usebruno/bruno/actions/workflows/tests.yml) [![Commit Activity](https://img.shields.io/github/commit-activity/m/usebruno/bruno)](https://github.com/usebruno/bruno/pulse) [![X](https://img.shields.io/twitter/follow/use_bruno?style=social&logo=x)](https://twitter.com/use_bruno) [![Website](https://img.shields.io/badge/Website-Visit-blue)](https://www.usebruno.com) [![Download](https://img.shields.io/badge/Download-Latest-brightgreen)](https://www.usebruno.com/downloads) [English](../../readme.md) | [Українська](./readme_ua.md) | [Русский](./readme_ru.md) | [Türkçe](./readme_tr.md) | [Deutsch](./readme_de.md) | [Français](./readme_fr.md) | [Português (BR)](./readme_pt_br.md) | [한국어](./readme_kr.md) | [বাংলা](./readme_bn.md) | [Español](./readme_es.md) | **Italiano** | [Română](./readme_ro.md) | [Polski](./readme_pl.md) | [简体中文](./readme_cn.md) | [正體中文](./readme_zhtw.md) | [العربية](./readme_ar.md) | [日本語](./readme_ja.md) | [ქართული](./readme_ka.md) Bruno è un nuovo ed innovativo API client, mirato a rivoluzionare lo status quo rappresentato da Postman e strumenti simili disponibili. Bruno 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. Puoi utilizzare Git o qualsiasi sistema di controllo che preferisci per collaborare sulle tue raccolte di API. Bruno 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) 📢 Guarda la nostra presentazione più recente alla conferenza India FOSS 3.0 [qui](https://www.youtube.com/watch?v=7bSMFpbcPiY) ![bruno](/assets/images/landing-2.png)

### Installazione Bruno è disponibile come download binario [sul nostro sito](https://www.usebruno.com/downloads) per Mac, Windows e Linux. Puoi installare Bruno anche tramite package manger come Homebrew, Chocolatey, Snap e Apt. ```sh # Su Mac come Homebrew brew install bruno # Su Windows come Chocolatey choco install bruno # Su Linux tramite Snap snap install bruno # Su Linux tramite Apt sudo mkdir -p /etc/apt/keyrings sudo apt update && sudo apt install gpg curl curl -fsSL "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x9FA6017ECABE0266" \ | gpg --dearmor \ | sudo tee /etc/apt/keyrings/bruno.gpg > /dev/null sudo chmod 644 /etc/apt/keyrings/bruno.gpg echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/bruno.gpg] http://debian.usebruno.com/ bruno stable" \ | sudo tee /etc/apt/sources.list.d/bruno.list sudo apt update && sudo apt install bruno ``` ### Funziona su diverse piattaforme 🖥️ ![bruno](/assets/images/run-anywhere.png)

### Collabora tramite Git 👩‍💻🧑‍💻 O con qualsiasi sistema di controllo di versioni a tua scelta ![bruno](/assets/images/version-control.png)

### Collegamenti importanti 📌 - [La nostra visione a lungo termine](https://github.com/usebruno/bruno/discussions/269) - [Roadmap](https://github.com/usebruno/bruno/discussions/384) - [Documentazione](https://docs.usebruno.com) - [Sito internet](https://www.usebruno.com) - [Prezzo](https://www.usebruno.com/pricing) - [Download](https://www.usebruno.com/downloads) ### Showcase 🎥 - [Testimonianze](https://github.com/usebruno/bruno/discussions/343) - [Centro di conoscenza](https://github.com/usebruno/bruno/discussions/386) - [Scriptmania](https://github.com/usebruno/bruno/discussions/385) ### Supporto ❤️ Woof! se ti piace il progetto, premi quel ⭐ pulsante !! ### Testimonianze condivise 📣 Se 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) ### Pubblica Bruno su un nuovo gestore di pacchetti Per favore vedi [qui](../../publishing.md) per accedere a più informazioni. ### Contribuire 👩‍💻🧑‍💻 Sono felice che vuoi migliorare Bruno. Per favore controlla la [guida per la partecipazione](../contributing/contributing_it.md) Anche 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. ### Autori
### Resta in contatto 🌐 [𝕏 (Twitter)](https://twitter.com/use_bruno)
[Sito internet](https://www.usebruno.com)
[Discord](https://discord.com/invite/KgcZUncpjq)
[LinkedIn](https://www.linkedin.com/company/usebruno) ### Marchio **Nome** `Bruno` è un marchio registrato appartenente a [Anoop M D](https://www.helloanoop.com/) **Logo** Il 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/) ### Licenza 📄 [MIT](../../license.md) ================================================ FILE: docs/readme/readme_ja.md ================================================
### Bruno - API の検証・動作テストのためのオープンソース IDE. [![GitHub version](https://badge.fury.io/gh/usebruno%2Fbruno.svg)](https://badge.fury.io/gh/usebruno%2Fbruno) [![CI](https://github.com/usebruno/bruno/actions/workflows/tests.yml/badge.svg?branch=main)](https://github.com/usebruno/bruno/actions/workflows/tests.yml) [![Commit Activity](https://img.shields.io/github/commit-activity/m/usebruno/bruno)](https://github.com/usebruno/bruno/pulse) [![X](https://img.shields.io/twitter/follow/use_bruno?style=social&logo=x)](https://twitter.com/use_bruno) [![Website](https://img.shields.io/badge/Website-Visit-blue)](https://www.usebruno.com) [![Download](https://img.shields.io/badge/Download-Latest-brightgreen)](https://www.usebruno.com/downloads) [English](../../readme.md) | [Українська](./readme_ua.md) | [Русский](./readme_ru.md) | [Türkçe](./readme_tr.md) | [Deutsch](./readme_de.md) | [Français](./readme_fr.md) | [Português (BR)](./readme_pt_br.md) | [한국어](./readme_kr.md) | [বাংলা](./readme_bn.md) | [Español](./readme_es.md) | [Italiano](./readme_it.md) | [Română](./readme_ro.md) | [Polski](./readme_pl.md) | [简体中文](./readme_cn.md) | [正體中文](./readme_zhtw.md) | [العربية](./readme_ar.md) | **日本語** | [ქართული](./readme_ka.md) Bruno は革新的な API クライアントです。Postman を代表する API クライアントツールの現状に一石を投じることを目指しています。 Bruno はローカルフォルダに直接コレクションを保存します。API リクエストの情報を保存するために Bru というプレーンテキストのマークアップ言語を採用しています。 Git や任意のバージョン管理システムを使って API コレクションを共同開発することもできます。 Bruno はオフラインのみで利用できます。Bruno にクラウド同期機能を追加する予定はありません。私たちはデータプライバシーを尊重しており、データはローカルに保存されるべきだと考えています。私たちの長期的なビジョンは[こちら](https://github.com/usebruno/bruno/discussions/269)をご覧ください。 [Bruno をダウンロード](https://www.usebruno.com/downloads) 📢 India FOSS 3.0 Conference での発表の様子は[こちら](https://www.youtube.com/watch?v=7bSMFpbcPiY)から ![bruno](/assets/images/landing-2.png)

### インストール方法 Bruno は[私たちのウェブサイト](https://www.usebruno.com/downloads)からバイナリをダウンロードできます。Mac, Windows, Linux に対応しています。 Homebrew, Chocolatey, Scoop, Snap, Flatpak, Apt などのパッケージマネージャからもインストール可能です。 ```sh # MacでHomebrewを使ってインストール brew install bruno # WindowsでChocolateyを使ってインストール choco install bruno # WindowsでScoopを使ってインストール scoop bucket add extras scoop install bruno # Windowsでwingetを使ってインストール winget install Bruno.Bruno # LinuxでSnapを使ってインストール snap install bruno # LinuxでFlatpakを使ってインストール flatpak install com.usebruno.Bruno # LinuxでAptを使ってインストール sudo mkdir -p /etc/apt/keyrings sudo apt update && sudo apt install gpg curl curl -fsSL "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x9FA6017ECABE0266" \ | gpg --dearmor \ | sudo tee /etc/apt/keyrings/bruno.gpg > /dev/null sudo chmod 644 /etc/apt/keyrings/bruno.gpg echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/bruno.gpg] http://debian.usebruno.com/ bruno stable" \ | sudo tee /etc/apt/sources.list.d/bruno.list sudo apt update && sudo apt install bruno ``` ### マルチプラットフォームでの実行に対応 🖥️ ![bruno](/assets/images/run-anywhere.png)

### Git との連携が可能 👩‍💻🧑‍💻 または任意のバージョン管理システムにも対応しています。 ![bruno](/assets/images/version-control.png)

### スポンサー #### ゴールドスポンサー #### シルバースポンサー #### ブロンズスポンサー ### 主要リンク 📌 - [私たちの長期ビジョン](https://github.com/usebruno/bruno/discussions/269) - [ロードマップ](https://github.com/usebruno/bruno/discussions/384) - [ドキュメント](https://docs.usebruno.com) - [Stack Overflow](https://stackoverflow.com/questions/tagged/bruno) - [ウェブサイト](https://www.usebruno.com) - [料金設定](https://www.usebruno.com/pricing) - [ダウンロード](https://www.usebruno.com/downloads) - [Github スポンサー](https://github.com/sponsors/helloanoop). ### Showcase 🎥 - [体験談](https://github.com/usebruno/bruno/discussions/343) - [ナレッジベース](https://github.com/usebruno/bruno/discussions/386) - [スクリプト集](https://github.com/usebruno/bruno/discussions/385) ### サポート ❤️ もし Bruno を気に入っていただいて、オープンソースの活動を支援していただけるなら、[Github Sponsors](https://github.com/sponsors/helloanoop)でスポンサーになることを考えてみてください。 ### 体験談のシェア 📣 Bruno が職場やチームで役立っているのであれば、[GitHub discussion 上であなたの体験談](https://github.com/usebruno/bruno/discussions/343)をシェアしていただくようお願いします。 ### 新しいパッケージマネージャへの公開 詳しくは[こちら](../publishing/publishing_ja.md)をご覧ください。 ### 連絡先 🌐 [𝕏 (Twitter)](https://twitter.com/use_bruno)
[Website](https://www.usebruno.com)
[Discord](https://discord.com/invite/KgcZUncpjq)
[LinkedIn](https://www.linkedin.com/company/usebruno) ### 商標について **名前** `Bruno`は[Anoop M D](https://www.helloanoop.com/)は取得している商標です。 **ロゴ** ロゴの出典は[OpenMoji](https://openmoji.org/library/emoji-1F436/)です。CC [BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/)でライセンスされています。 ### 貢献するには 👩‍💻🧑‍💻 Bruno を改善していただけるのは歓迎です。[コントリビュートガイド](../contributing/contributing_ja.md)をご覧ください。 もしコードによる貢献ができない場合でも、あなたのユースケースを解決するために遠慮なくバグ報告や機能リクエストを出してください。 ### 開発者
### ライセンス 📄 [MIT](../../license.md) ================================================ FILE: docs/readme/readme_ka.md ================================================
### ბრუნო - ღია წყაროების IDE API-ების შესწავლისა და ტესტირებისათვის. [![GitHub version](https://badge.fury.io/gh/usebruno%2Fbruno.svg)](https://badge.fury.io/gh/usebruno%2Fbruno) [![CI](https://github.com/usebruno/bruno/actions/workflows/tests.yml/badge.svg?branch=main)](https://github.com/usebruno/bruno/actions/workflows/tests.yml) [![Commit Activity](https://img.shields.io/github/commit-activity/m/usebruno/bruno)](https://github.com/usebruno/bruno/pulse) [![X](https://img.shields.io/twitter/follow/use_bruno?style=social&logo=x)](https://twitter.com/use_bruno) [![Website](https://img.shields.io/badge/Website-Visit-blue)](https://www.usebruno.com) [![Download](https://img.shields.io/badge/Download-Latest-brightgreen)](https://www.usebruno.com/downloads) [English](../../readme.md) | [Українська](./readme_ua.md) | [Русский](./readme_ru.md) | [Türkçe](./readme_tr.md) | [Deutsch](./readme_de.md) | [Français](./readme_fr.md) | [Português (BR)](./readme_pt_br.md) | [한국어](./readme_kr.md) | [বাংলা](./readme_bn.md) | [Español](./readme_es.md) | [Italiano](./readme_it.md) | [Română](./readme_ro.md) | [Polski](./readme_pl.md) | [简体中文](./readme_cn.md) | [正體中文](./readme_zhtw.md) | [العربية](./readme_ar.md) | [日本語](./readme_ja.md) | **ქართული** ბრუნო არის ახალი და ინოვაციური API კლიენტი, რომელიც მიზნად ისახავს პოსტმანისა და მსგავსი ინსტრუმენტების არსებული მდგომარეობის რევოლუციას. ბრუნო თქვენი კოლექციების შენახვას უშუალოდ თქვენს ფაილური სისტემის ერთ-ერთ საქაღალოში ახდენს. ჩვენ ვხმარობთ უბრალო ტექსტურ მარკაპ ენის, Bru-ს, API მოთხოვნების შესახებ ინფორმაციის შენახვისთვის. თქვენ შეგიძლიათ გამოიყენოთ Git ან ნებისმიერი ვერსიის კონტროლის სისტემა თქვენი API კოლექციების გასაზიარებლად. ბრუნო მხოლოდ ოფლაინ რეჟიმში მუშაობს. ბრუნოში ღრუბლური სინქრონიზაციის დამატების გეგმები არ არის. ჩვენ ვაფასებთ თქვენი მონაცემების პრივატობას და creemos, რომ ისინი თქვენს მოწყობილობაში უნდა დარჩეს. წაიკითხეთ ჩვენი გრძელვადიანი ხედვა [აქ](https://github.com/usebruno/bruno/discussions/269) [დამატებით ბრუნო](https://www.usebruno.com/downloads) 📢 შეიტყვეთ ჩვენი უახლესი საუბრის შესახებ India FOSS 3.0 კონფერენციაზე [აქ](https://www.youtube.com/watch?v=7bSMFpbcPiY) ![bruno](../../assets/images/landing-2.png)

### ინსტალაცია ბრუნო ხელმისაწვდომია როგორც ბინარული ჩამოტვირთვა [ჩვენ的网站上](https://www.usebruno.com/downloads) Mac-ის, Windows-ისა და Linux-ისთვის. თქვენ ასევე შეგიძლიათ დააინსტალიროთ ბრუნო პაკეტის მენეჯერების საშუალებით, როგორიცაა Homebrew, Chocolatey, Scoop, Snap, Flatpak და Apt. ```sh # Mac-ზე Homebrew-ს საშუალებით brew install bruno # Windows-ზე Chocolatey-ს საშუალებით choco install bruno # Windows-ზე Scoop-ის საშუალებით scoop bucket add extras scoop install bruno # Windows-ზე winget-ის საშუალებით winget install Bruno.Bruno # Linux-ზე Snap-ის საშუალებით snap install bruno # Linux-ზე Flatpak-ის საშუალებით flatpak install com.usebruno.Bruno # Linux-ზე Apt-ის საშუალებით sudo mkdir -p /etc/apt/keyrings sudo apt update && sudo apt install gpg curl curl -fsSL "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x9FA6017ECABE0266" \ | gpg --dearmor \ | sudo tee /etc/apt/keyrings/bruno.gpg > /dev/null sudo chmod 644 /etc/apt/keyrings/bruno.gpg echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/bruno.gpg] http://debian.usebruno.com/ bruno stable" \ | sudo tee /etc/apt/sources.list.d/bruno.list sudo apt update && sudo apt install bruno ``` ### პლატფორმებს შორის მუშაობა 🖥️ ![bruno](../../assets/images/run-anywhere.png)

### თანამშრომლობა Git-ის საშუალებით 👩‍💻🧑‍💻 ან ნებისმიერი ვერსიის კონტროლის სისტემის საშუალებით ![bruno](../../assets/images/version-control.png)

### სპონსორები #### ოქროს სპონსორები #### ვერცხლის სპონსორები #### ბრინჯის სპონსორები ### მნიშვნელოვანი ბმულები 📌 - [ჩვენი გრძელვადიანი ხედვა](https://github.com/usebruno/bruno/discussions/269) - [გეგმა](https://github.com/usebruno/bruno/discussions/384) - [დოკუმენტაცია](https://docs.usebruno.com) - [Stack Overflow](https://stackoverflow.com/questions/tagged/bruno) - [ვებსაიტი](https://www.usebruno.com) - [ფასები](https://www.usebruno.com/pricing) - [დამატება](https://www.usebruno.com/downloads) - [GitHub სპონსორები](https://github.com/sponsors/helloanoop). ### ვიტრინა 🎥 - [მოწონებები](https://github.com/usebruno/bruno/discussions/343) - [მეცნიერების ჰაბი](https://github.com/usebruno/bruno/discussions/386) - [Scriptmania](https://github.com/usebruno/bruno/discussions/385) ### მხარდაჭერა ❤️ თუ გიყვართ ბრუნო და გინდათ მხარი დაუჭიროთ ჩვენს ღია წყაროების მუშაობას, გაითვალისწინეთ ჩვენი დახმარება [GitHub სპონსორების საშუალებით](https://github.com/sponsors/helloanoop). ### გააზიარეთ მოწმობები 📣 თუ ბრუნო დაგეხმარათ თქვენს სამუშაოში და გუნდებში, გთხოვთ, არ დაგავიწყდეთ ჩვენი [მოწონებების გაზიარება ჩვენს GitHub განხილვაში](https://github.com/usebruno/bruno/discussions/343) ### ახალი პაკეტის მენეჯერებში გამოქვეყნება იხილეთ [აქ](../../publishing.md) მეტი ინფორმაციისათვის. ### დაინტერესდით 🌐 [𝕎 (Twitter)](https://twitter.com/use_bruno)
[ვებსაიტი](https://www.usebruno.com)
[Discord](https://discord.com/invite/KgcZUncpjq)
[LinkedIn](https://www.linkedin.com/company/usebruno) ### სავაჭრო ნიშანი **სახელი** `ბრუნო` არის სავაჭრო ნიშანი, რომელსაც ფლობს [ანუპ მ. დ.](https://www.helloanoop.com/) **ლოგო** ლოგო არის [OpenMoji](https://openmoji.org/library/emoji-1F436/) სურათებიდან. ლიცენზია: CC [BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/) ### თანამშრომლობა 👩‍💻🧑‍💻 მიხარია, რომ დაინტერესებული ხართ ბრუნოს გაუმჯობესებით. გთხოვთ, გადახედეთ [თანამშრომლობის სახელმძღვანელოს](../../contributing.md) თუნდაც ვერ მოახერხოთ კოდის საშუალებით კონტრიბუცია, ნუ ინანებთ პრობლემების და ფუნქციის მოთხოვნების ჩაწერას, რომლებიც უნდა განხორციელდეს თქვენი შემთხვევის გადასაჭრელად. ### ავტორები
### ლიცენზია 📄 [MIT](../../license.md) ================================================ FILE: docs/readme/readme_kr.md ================================================
### Bruno - API 탐색 및 테스트를 위한 오픈소스 IDE. [![GitHub version](https://badge.fury.io/gh/usebruno%2Fbruno.svg)](https://badge.fury.io/gh/usebruno%2Fbruno) [![CI](https://github.com/usebruno/bruno/actions/workflows/tests.yml/badge.svg?branch=main)](https://github.com/usebruno/bruno/actions/workflows/tests.yml) [![Commit Activity](https://img.shields.io/github/commit-activity/m/usebruno/bruno)](https://github.com/usebruno/bruno/pulse) [![X](https://img.shields.io/twitter/follow/use_bruno?style=social&logo=x)](https://twitter.com/use_bruno) [![Website](https://img.shields.io/badge/Website-Visit-blue)](https://www.usebruno.com) [![Download](https://img.shields.io/badge/Download-Latest-brightgreen)](https://www.usebruno.com/downloads) [English](../../readme.md) | [Українська](./readme_ua.md) | [Русский](./readme_ru.md) | [Türkçe](./readme_tr.md) | [Deutsch](./readme_de.md) | [Français](./readme_fr.md) | [Português (BR)](./readme_pt_br.md) | **한국어** | [বাংলা](./readme_bn.md) | [Español](./readme_es.md) | [Italiano](./readme_it.md) | [Română](./readme_ro.md) | [Polski](./readme_pl.md) | [简体中文](./readme_cn.md) | [正體中文](./readme_zhtw.md) | [العربية](./readme_ar.md) | [日本語](./readme_ja.md) | [ქართული](./readme_ka.md) Bruno는 새롭고 혁신적인 API 클라이언트로, Postman과 유사한 툴들을 혁신하는 것을 목표로 합니다. Bruno는 사용자의 컬렉션을 파일 시스템의 폴더에 직접 저장합니다. 일반 텍스트 마크업 언어인 Bru를 사용해 API 요청에 대한 정보를 저장합니다. Git 또는 원하는 버전 관리 도구를 사용하여 API 컬렉션을 연동할 수 있습니다. 브루는 오프라인 전용입니다. 브루노에 클라우드 동기화 기능을 추가할 계획은 없습니다. 저희는 사용자의 데이터 프라이버시를 소중히 여기며, 데이터는 사용자의 기기에 남아 있어야 한다고 믿습니다. 장기 비전 읽기 [링크](https://github.com/usebruno/bruno/discussions/269) 📢 Watch our recent talk at India FOSS 3.0 Conference [here](https://www.youtube.com/watch?v=7bSMFpbcPiY) ![bruno](/assets/images/landing-2.png)

### 설치 Bruno 는 여기에서 다운로드 받을 수 있습니다.[링크](https://www.usebruno.com/downloads) (맥, 윈도우, 리눅스) Homebrew, Chocolatey, Snap, Apt 같은 패키지 관리자를 통해서도 Bruno를 설치할 수 있습니다. ```sh # On Mac via Homebrew brew install bruno # On Windows via Chocolatey choco install bruno # On Linux via Snap snap install bruno # On Linux via Apt sudo mkdir -p /etc/apt/keyrings sudo apt update && sudo apt install gpg curl curl -fsSL "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x9FA6017ECABE0266" \ | gpg --dearmor \ | sudo tee /etc/apt/keyrings/bruno.gpg > /dev/null sudo chmod 644 /etc/apt/keyrings/bruno.gpg echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/bruno.gpg] http://debian.usebruno.com/ bruno stable" \ | sudo tee /etc/apt/sources.list.d/bruno.list sudo apt update && sudo apt install bruno ``` ### 여러 플랫폼에서 실행하세요. 🖥️ ![bruno](/assets/images/run-anywhere.png)

### Git과 연동하세요. 👩‍💻🧑‍💻 또는 원하는 버전 관리 시스템을 선택하세요. ![bruno](/assets/images/version-control.png)

### 중요 링크 📌 - [Our Long Term Vision](https://github.com/usebruno/bruno/discussions/269) - [Roadmap](https://github.com/usebruno/bruno/discussions/384) - [Documentation](https://docs.usebruno.com) - [Website](https://www.usebruno.com) - [Pricing](https://www.usebruno.com/pricing) - [Download](https://www.usebruno.com/downloads) ### 쇼케이스 🎥 - [Testimonials](https://github.com/usebruno/bruno/discussions/343) - [Knowledge Hub](https://github.com/usebruno/bruno/discussions/386) - [Scriptmania](https://github.com/usebruno/bruno/discussions/385) ### 지원 ❤️ 프로젝트가 마음에 들면 ⭐ 버튼을 눌러 주세요. ### 후기 공유 📣 Bruno가 여러분과 여러분의 팀에 도움이 되었다면, 잊지 말고 공유해 주세요. [GitHub discussion 공유 링크](https://github.com/usebruno/bruno/discussions/343) ### 새 패키지 관리자에게 게시 더 많은 정보를 확인하시려면 링크를 클릭해 주세요. [배포 가이드](../../publishing.md) ### 컨트리뷰트 👩‍💻🧑‍💻 컨트리뷰트에 관심이 있으시면 링크를 참고해 주세요. [컨트리뷰트 가이드](../contributing/contributing_kr.md) 코드를 통해 기여할 수 없더라도 사용 사례를 해결하기 위해 구현이 필요한 버그나 기능 요청을 주저하지 마시고 제출해 주세요. ### Authors
### Stay in touch 🌐 [𝕏 (Twitter)](https://twitter.com/use_bruno)
[Website](https://www.usebruno.com)
[Discord](https://discord.com/invite/KgcZUncpjq)
[LinkedIn](https://www.linkedin.com/company/usebruno) ### Trademark **Name** `Bruno` is a trademark held by [Anoop M D](https://www.helloanoop.com/) **Logo** The 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/) ### License 📄 [MIT](../../license.md) ================================================ FILE: docs/readme/readme_nl.md ================================================
### Bruno - Open source IDE voor het verkennen en testen van API's. [![GitHub version](https://badge.fury.io/gh/usebruno%2Fbruno.svg)](https://badge.fury.io/gh/usebruno%2Fbruno) [![CI](https://github.com/usebruno/bruno/actions/workflows/tests.yml/badge.svg?branch=main)](https://github.com/usebruno/bruno/actions/workflows/tests.yml) [![Commit Activity](https://img.shields.io/github/commit-activity/m/usebruno/bruno)](https://github.com/usebruno/bruno/pulse) [![X](https://img.shields.io/twitter/follow/use_bruno?style=social&logo=x)](https://twitter.com/use_bruno) [![Website](https://img.shields.io/badge/Website-Visit-blue)](https://www.usebruno.com) [![Download](https://img.shields.io/badge/Download-Latest-brightgreen)](https://www.usebruno.com/downloads) [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) Bruno is een nieuwe en innovatieve API-client, gericht op het revolutioneren van de status quo die wordt vertegenwoordigd door Postman en vergelijkbare tools. Bruno 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. Je kunt Git of elke versiebeheertool naar keuze gebruiken om samen te werken aan je API-collecties. Bruno 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) [Download Bruno](https://www.usebruno.com/downloads) 📢 Bekijk onze recente presentatie op de India FOSS 3.0 Conference [hier](https://www.youtube.com/watch?v=7bSMFpbcPiY) ![bruno](/assets/images/landing-2.png)

### Installatie Bruno is beschikbaar als binaire download [op onze website](https://www.usebruno.com/downloads) voor Mac, Windows en Linux. Je kunt Bruno ook installeren via pakketbeheerders zoals Homebrew, Chocolatey, Scoop, Snap, Flatpak en Apt. ```sh # Op Mac via Homebrew brew install bruno # Op Windows via Chocolatey choco install bruno # Op Windows via Scoop scoop bucket add extras scoop install bruno # Op Windows via winget winget install Bruno.Bruno # Op Linux via Snap snap install bruno # Op Linux via Flatpak flatpak install com.usebruno.Bruno # Op Linux via Apt sudo mkdir -p /etc/apt/keyrings sudo apt update && sudo apt install gpg curl curl -fsSL "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x9FA6017ECABE0266" \ | gpg --dearmor \ | sudo tee /etc/apt/keyrings/bruno.gpg > /dev/null sudo chmod 644 /etc/apt/keyrings/bruno.gpg echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/bruno.gpg] http://debian.usebruno.com/ bruno stable" \ | sudo tee /etc/apt/sources.list.d/bruno.list sudo apt update && sudo apt install bruno ``` ### Draai op meerdere platformen 🖥️ ![bruno](/assets/images/run-anywhere.png)

### Samenwerken via Git 👩‍💻🧑‍💻 Of elk versiebeheersysteem naar keuze ![bruno](/assets/images/version-control.png)

### Sponsors #### Gouden Sponsors #### Zilveren Sponsors #### Bronzen Sponsors ### Belangrijke Links 📌 - [Onze Langetermijnvisie](https://github.com/usebruno/bruno/discussions/269) - [Roadmap](https://github.com/usebruno/bruno/discussions/384) - [Documentatie](https://docs.usebruno.com) - [Stack Overflow](https://stackoverflow.com/questions/tagged/bruno) - [Website](https://www.usebruno.com) - [Prijzen](https://www.usebruno.com/pricing) - [Download](https://www.usebruno.com/downloads) - [GitHub Sponsors](https://github.com/sponsors/helloanoop) ### Showcase 🎥 - [Getuigenissen](https://github.com/usebruno/bruno/discussions/343) - [Kenniscentrum](https://github.com/usebruno/bruno/discussions/386) - [Scriptmania](https://github.com/usebruno/bruno/discussions/385) ### Ondersteuning ❤️ Als 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). ### Deel Getuigenissen 📣 Als 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). ### Blijf in contact 🌐 [𝕏 (Twitter)](https://twitter.com/use_bruno)
[Website](https://www.usebruno.com)
[Discord](https://discord.com/invite/KgcZUncpjq)
[LinkedIn](https://www.linkedin.com/company/usebruno) ### Handelsmerk **Naam** `Bruno` is een handelsmerk in bezit van [Anoop M D](https://www.helloanoop.com/). **Logo** Het 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/) ### Bijdragen 👩‍💻🧑‍💻 Ik ben blij dat je Bruno wilt verbeteren. Bekijk de [bijdragegids](contributing.md). Zelfs 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. ### Auteurs
### Licentie 📄 [MIT](../../license.md) ================================================ FILE: docs/readme/readme_pl.md ================================================
### Bruno - Otwartoźródłowe IDE do eksploracji i testów APIs. [![GitHub version](https://badge.fury.io/gh/usebruno%2Fbruno.svg)](https://badge.fury.io/gh/usebruno%2Fbruno) [![CI](https://github.com/usebruno/bruno/actions/workflows/tests.yml/badge.svg?branch=main)](https://github.com/usebruno/bruno/actions/workflows/tests.yml) [![Commit Activity](https://img.shields.io/github/commit-activity/m/usebruno/bruno)](https://github.com/usebruno/bruno/pulse) [![X](https://img.shields.io/twitter/follow/use_bruno?style=social&logo=x)](https://twitter.com/use_bruno) [![Website](https://img.shields.io/badge/Website-Visit-blue)](https://www.usebruno.com) [![Download](https://img.shields.io/badge/Download-Latest-brightgreen)](https://www.usebruno.com/downloads) [English](../../readme.md) | [Українська](./readme_ua.md) | [Русский](./readme_ru.md) | [Türkçe](./readme_tr.md) | [Deutsch](./readme_de.md) | [Français](./readme_fr.md) | [Português (BR)](./readme_pt_br.md) | [한국어](./readme_kr.md) | [বাংলা](./readme_bn.md) | [Español](./readme_es.md) | [Italiano](./readme_it.md) | [Română](./readme_ro.md) | **Polski** | [简体中文](./readme_cn.md) | [正體中文](./readme_zhtw.md) | [العربية](./readme_ar.md) | [日本語](./readme_ja.md) | [ქართული](./readme_ka.md) Bruno to nowy i innowacyjny klient API, którego celem jest zrewolucjonizowanie status quo reprezentowanego przez narzędzia takie jak Postman. Bruno 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. Możesz użyć Git lub dowolnego systemu kontroli wersji do współpracy nad swoimi kolekcjami API. Bruno 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) 📢 Obejrzyj naszą ostatnią rozmowę na konferencji India FOSS 3.0 [tutaj](https://www.youtube.com/watch?v=7bSMFpbcPiY) ![bruno](/assets/images/landing-2.png)

### Instalacja Bruno jest dostępny jako plik binarny do pobrania [na naszej stronie internetowej](https://www.usebruno.com/downloads) dla Mac, Windows i Linux. Możesz również zainstalować Bruno za pomocą menedżerów pakietów, takich jak Homebrew, Chocolatey, Scoop, Snap i Apt. ```sh # On Mac via Homebrew brew install bruno # On Windows via Chocolatey choco install bruno # On Windows via Scoop scoop bucket add extras scoop install bruno # On Windows via winget winget install Bruno.Bruno # On Linux via Snap snap install bruno # On Linux via Flatpak flatpak install com.usebruno.Bruno # On Linux via Apt sudo mkdir -p /etc/apt/keyrings sudo apt update && sudo apt install gpg curl curl -fsSL "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x9FA6017ECABE0266" \ | gpg --dearmor \ | sudo tee /etc/apt/keyrings/bruno.gpg > /dev/null sudo chmod 644 /etc/apt/keyrings/bruno.gpg echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/bruno.gpg] http://debian.usebruno.com/ bruno stable" \ | sudo tee /etc/apt/sources.list.d/bruno.list sudo apt update && sudo apt install bruno ``` ### Uruchom na wielu platformach 🖥️ ![bruno](/assets/images/run-anywhere.png)

### Współpracuj przez Git 👩‍💻🧑‍💻 Lub dowolny inny system kontroli wersji, który wybierzesz ![bruno](/assets/images/version-control.png)

### Ważne Linki 📌 - [Nasza Długoterminowa Wizja](https://github.com/usebruno/bruno/discussions/269) - [Mapa Drogi](https://github.com/usebruno/bruno/discussions/384) - [Dokumentacja](https://docs.usebruno.com) - [Stack Overflow](https://stackoverflow.com/questions/tagged/bruno) - [Strona Internetowa](https://www.usebruno.com) - [Cennik](https://www.usebruno.com/pricing) - [Pobieranie](https://www.usebruno.com/downloads) - [Sponsorzy GitHub](https://github.com/sponsors/helloanoop). ### Zobacz 🎥 - [Opinie](https://github.com/usebruno/bruno/discussions/343) - [Centrum Wiedzy](https://github.com/usebruno/bruno/discussions/386) - [Scriptmania](https://github.com/usebruno/bruno/discussions/385) ### Wsparcie ❤️ Jeśli podoba Ci się Bruno i chcesz wspierać naszą pracę opensource, rozważ sponsorowanie nas przez [Sponsorzy GitHub](https://github.com/sponsors/helloanoop). ### Udostępnij Opinie 📣 Jeś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) ### Publikowanie w Nowych Menedżerach Pakietów Więcej informacji znajdziesz [tutaj](../publishing/publishing_pl.md). ### Współpraca 👩‍💻🧑‍💻 Cieszymy się, że chcesz udoskonalić bruno. Proszę sprawdź [przewodnik współpracy](../contributing/contributing_pl.md) Nawet 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. ### Autorzy
### Pozostań w kontakcie 🌐 [𝕏 (Twitter)](https://twitter.com/use_bruno)
[Strona Internetowa](https://www.usebruno.com)
[Discord](https://discord.com/invite/KgcZUncpjq)
[LinkedIn](https://www.linkedin.com/company/usebruno) ### Znak Towarowy **Nazwa** `Bruno` jest znakiem towarowym należącym do [Anoop M D](https://www.helloanoop.com/) **Logo** Logo pochodzi z [OpenMoji](https://openmoji.org/library/emoji-1F436/). Licencja: CC [BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/) ### Licencja 📄 [MIT](../../license.md) ================================================ FILE: docs/readme/readme_pt_br.md ================================================
### Bruno - IDE de código aberto para explorar e testar APIs. [![GitHub version](https://badge.fury.io/gh/usebruno%2Fbruno.svg)](https://badge.fury.io/gh/usebruno%2Fbruno) [![CI](https://github.com/usebruno/bruno/actions/workflows/tests.yml/badge.svg?branch=main)](https://github.com/usebruno/bruno/actions/workflows/tests.yml) [![Commit Activity](https://img.shields.io/github/commit-activity/m/usebruno/bruno)](https://github.com/usebruno/bruno/pulse) [![X](https://img.shields.io/twitter/follow/use_bruno?style=social&logo=x)](https://twitter.com/use_bruno) [![Website](https://img.shields.io/badge/Website-Visit-blue)](https://www.usebruno.com) [![Download](https://img.shields.io/badge/Download-Latest-brightgreen)](https://www.usebruno.com/downloads) [English](../../readme.md) | [Українська](./readme_ua.md) | [Русский](./readme_ru.md) | [Türkçe](./readme_tr.md) | [Deutsch](./readme_de.md) | [Français](./readme_fr.md) | **Português (BR)** | [한국어](./readme_kr.md) | [বাংলা](./readme_bn.md) | [Español](./readme_es.md) | [Italiano](./readme_it.md) | [Română](./readme_ro.md) | [Polski](./readme_pl.md) | [简体中文](./readme_cn.md) | [正體中文](./readme_zhtw.md) | [العربية](./readme_ar.md) | [日本語](./readme_ja.md) | [ქართული](./readme_ka.md) Bruno é um novo e inovador cliente de API, com o objetivo de revolucionar o status quo representado por ferramentas como o Postman e outras semelhantes. Bruno 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. Você pode usar o Git ou qualquer sistema de controle de versão de sua escolha para colaborar em suas coleções de API. Bruno é 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). 📢 Assista à nossa palestra recente na India FOSS 3.0 Conference [aqui](https://www.youtube.com/watch?v=7bSMFpbcPiY). ![bruno](../../assets/images/landing-2.png)

### Instalação Bruno está disponível para download como binário [em nosso site](https://www.usebruno.com/downloads) para Mac, Windows e Linux. Você também pode instalar o Bruno via gerenciadores de pacotes como Homebrew, Chocolatey, Snap e Apt. ```sh # No Mac via Homebrew brew install bruno # No Windows via Chocolatey choco install bruno # No Windows via Scoop scoop bucket add extras scoop install bruno # No Windows via winget winget install Bruno.Bruno # No Linux via Snap snap install bruno # No Linux via Flatpak flatpak install com.usebruno.Bruno # No Linux via Apt sudo mkdir -p /etc/apt/keyrings sudo apt update && sudo apt install gpg curl curl -fsSL "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x9FA6017ECABE0266" \ | gpg --dearmor \ | sudo tee /etc/apt/keyrings/bruno.gpg > /dev/null sudo chmod 644 /etc/apt/keyrings/bruno.gpg echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/bruno.gpg] http://debian.usebruno.com/ bruno stable" \ | sudo tee /etc/apt/sources.list.d/bruno.list sudo apt update && sudo apt install bruno ``` ### Execute em várias plataformas 🖥️ ![bruno](../../assets/images/run-anywhere.png)

### Colaboração via Git 👩‍💻🧑‍💻 Ou qualquer sistema de controle de versão de sua escolha. ![bruno](../../assets/images/version-control.png)

### Apoiadores #### Apoiadores Gold #### Apoiadores Silver #### Apoiadores Bronze ### Links Importantes 📌 - [Nossa Visão de Longo Prazo](https://github.com/usebruno/bruno/discussions/269) - [Roadmap](https://github.com/usebruno/bruno/discussions/384) - [Documentação](https://docs.usebruno.com) - [Stack Overflow](https://stackoverflow.com/questions/tagged/bruno) - [Website](https://www.usebruno.com) - [Preços](https://www.usebruno.com/pricing) - [Download](https://www.usebruno.com/downloads) - [GitHub Sponsors](https://github.com/sponsors/helloanoop) ### Showcase 🎥 - [Depoimentos](https://github.com/usebruno/bruno/discussions/343) - [Hub de Conhecimento](https://github.com/usebruno/bruno/discussions/386) - [Scriptmania](https://github.com/usebruno/bruno/discussions/385) ### Apoie ❤️ Au-au! Se você gosta do projeto e deseja apoiar nosso trabalho, considere nos ajudando via [GitHub Sponsors](https://github.com/sponsors/helloanoop). ### Compartilhe sua experiência 📣 Se 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). ### Publicando em Novos Gerenciadores de Pacotes Por favor, verifique [aqui](../publishing/publishing_pt_br.md) mais informações. ### Mantenha Contato 🌐 [𝕏 (Twitter)](https://twitter.com/use_bruno)
[Website](https://www.usebruno.com)
[Discord](https://discord.com/invite/KgcZUncpjq)
[LinkedIn](https://www.linkedin.com/company/usebruno) ### Trademark **Nome** `Bruno` é uma marca registrada de [Anoop M D](https://www.helloanoop.com/). **Logo** A 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/). ### Colabore 👩‍💻🧑‍💻 Fico feliz que você queira melhorar o Bruno. Por favor, confira o [guia de colaboração](../contributing/contributing_pt_br.md). Mesmo 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. ### Contribuidores
### Licença 📄 [MIT](../../license.md) ================================================ FILE: docs/readme/readme_ro.md ================================================
### Bruno - Mediu integrat de dezvoltare cu sursă deschisă pentru explorarea și testarea API-urilor. [![GitHub version](https://badge.fury.io/gh/usebruno%2Fbruno.svg)](https://badge.fury.io/gh/usebruno%2Fbruno) [![CI](https://github.com/usebruno/bruno/actions/workflows/tests.yml/badge.svg?branch=main)](https://github.com/usebruno/bruno/actions/workflows/tests.yml) [![Commit Activity](https://img.shields.io/github/commit-activity/m/usebruno/bruno)](https://github.com/usebruno/bruno/pulse) [![X](https://img.shields.io/twitter/follow/use_bruno?style=social&logo=x)](https://twitter.com/use_bruno) [![Website](https://img.shields.io/badge/Website-Visit-blue)](https://www.usebruno.com) [![Download](https://img.shields.io/badge/Download-Latest-brightgreen)](https://www.usebruno.com/downloads) [English](../../readme.md) | [Українська](./readme_ua.md) | [Русский](./readme_ru.md) | [Türkçe](./readme_tr.md) | [Deutsch](./readme_de.md) | [Français](./readme_fr.md) | [Português (BR)](./readme_pt_br.md) | [한국어](./readme_kr.md) | [বাংলা](./readme_bn.md) | [Español](./readme_es.md) | [Italiano](./readme_it.md) | **Română** | [Polski](./readme_pl.md) | [简体中文](./readme_cn.md) | [正體中文](./readme_zhtw.md) | [العربية](./readme_ar.md) | [日本語](./readme_ja.md) | [ქართული](./readme_ka.md) Bruno este un client API nou și inovativ, care vizează să revoluționeze status quo-ul reprezentat de Postman și alte instrumente similare. Bruno 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. Puteți folosi Git sau orice altă unealtă de control al versiunii la alegere pentru a colabora la colecțiile API voastre. Bruno 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) 📢 Priviți prezentarea noastră recentă de la India FOSS 3.0 Conference [aici](https://www.youtube.com/watch?v=7bSMFpbcPiY) ![bruno](/assets/images/landing-2.png)

### Instalarea Bruno este disponibil ca descărcare binară [pe website-ul nostru](https://www.usebruno.com/downloads) pentru Mac, Windows și Linux. De asemenea, puteţi instala Bruno cu un gestionar de pachete precum Homebrew, Chocolatey, Snap şi Apt. ```sh # Pe Mac cu Homebrew brew install bruno # Pe Windows cu Chocolatey choco install bruno # Pe Linux cu Snap snap install bruno # Pe Linux cu Apt sudo mkdir -p /etc/apt/keyrings sudo apt update && sudo apt install gpg curl curl -fsSL "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x9FA6017ECABE0266" \ | gpg --dearmor \ | sudo tee /etc/apt/keyrings/bruno.gpg > /dev/null sudo chmod 644 /etc/apt/keyrings/bruno.gpg echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/bruno.gpg] http://debian.usebruno.com/ bruno stable" \ | sudo tee /etc/apt/sources.list.d/bruno.list sudo apt update && sudo apt install bruno ``` ### Utilizați pe mai multe platforme 🖥️ ![bruno](/assets/images/run-anywhere.png)

### Colaborați cu Git 👩‍💻🧑‍💻 Sau orice unealtă de control al versiunii la alegere ![bruno](/assets/images/version-control.png)

### Linkuri importante 📌 - [Viziunea noastră pe termen lung](https://github.com/usebruno/bruno/discussions/269) - [Roadmap](https://github.com/usebruno/bruno/discussions/384) - [Documentație](https://docs.usebruno.com) - [Stack Overflow](https://stackoverflow.com/questions/tagged/bruno) - [Website](https://www.usebruno.com) - [Prețuri](https://www.usebruno.com/pricing) - [Descărcări](https://www.usebruno.com/downloads) - [Sponsori GitHub](https://github.com/sponsors/helloanoop). ### Vitrina 🎥 - [Recenzii](https://github.com/usebruno/bruno/discussions/343) - [Centrul de cunoștințe](https://github.com/usebruno/bruno/discussions/386) - [Scriptmania](https://github.com/usebruno/bruno/discussions/385) ### Sprijiniți ❤️ Dacă 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). ### Distribuiți recenziile 📣 Dacă 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) ### Publicarea la gestionari de pachete noi Vă rugăm să citiţi [aici](../publishing/publishing_ro.md) pentru mai multă informaţie. ### Contribuiți 👩‍💻🧑‍💻 Mă bucur că doriți să îmbunătățiți Bruno. Vă rugăm să consultați [ghidul pentru contribuire](../contributing/contributing_ro.md) Chiar 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. ### Autori
### Păstrați legătura 🌐 [𝕏 (Twitter)](https://twitter.com/use_bruno)
[Website](https://www.usebruno.com)
[Discord](https://discord.com/invite/KgcZUncpjq)
[LinkedIn](https://www.linkedin.com/company/usebruno) ### Marcă comercială **Nume** `Bruno` este o marcă deținută de [Anoop M D](https://www.helloanoop.com/) **Logo** Logo-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/) ### Licența 📄 [MIT](../../license.md) ================================================ FILE: docs/readme/readme_ru.md ================================================
### Bruno - IDE с открытым исходным кодом для изучения и тестирования API. [![GitHub version](https://badge.fury.io/gh/usebruno%2Fbruno.svg)](https://badge.fury.io/gh/usebruno%2Fbruno) [![CI](https://github.com/usebruno/bruno/actions/workflows/tests.yml/badge.svg?branch=main)](https://github.com/usebruno/bruno/actions/workflows/tests.yml) [![Commit Activity](https://img.shields.io/github/commit-activity/m/usebruno/bruno)](https://github.com/usebruno/bruno/pulse) [![X](https://img.shields.io/twitter/follow/use_bruno?style=social&logo=x)](https://twitter.com/use_bruno) [![Website](https://img.shields.io/badge/Website-Visit-blue)](https://www.usebruno.com) [![Download](https://img.shields.io/badge/Download-Latest-brightgreen)](https://www.usebruno.com/downloads) [English](../../readme.md) | [Українська](./readme_ua.md) | **Русский** | [Türkçe](./readme_tr.md) | [Deutsch](./readme_de.md) | [Français](./readme_fr.md) | [Português (BR)](./readme_pt_br.md) | [한국어](./readme_kr.md) | [বাংলা](./readme_bn.md) | [Español](./readme_es.md) | [Italiano](./readme_it.md) | [Română](./readme_ro.md) | [Polski](./readme_pl.md) | [简体中文](./readme_cn.md) | [正體中文](./readme_zhtw.md) | [العربية](./readme_ar.md) | [日本語](./readme_ja.md) | [ქართული](./readme_ka.md) Bruno - новый и инновационный клиент API, направленный на революцию в установившейся ситуации, представленной Postman и подобными инструментами. Bruno хранит ваши коллекции непосредственно в папке в вашей файловой системе. Для сохранения информации об API-запросах мы используем язык Bru. Для совместной работы над коллекциями API можно использовать git или любой другой контроль версий по вашему выбору. Bruno работает только в автономном режиме. Добавление облачной синхронизации в Bruno не планируется. Мы ценим конфиденциальность ваших данных и считаем, что они должны оставаться на вашем устройстве. Ознакомьтесь с нашим долгосрочным видением [здесь](https://github.com/usebruno/bruno/discussions/269) ![bruno](/assets/images/landing-2.png)

### Работа на нескольких платформах 🖥️ ![bruno](/assets/images/run-anywhere.png)

### Совместная работа через Git 👩‍💻🧑‍💻 Или другая система контроля версий по вашему выбору ![bruno](/assets/images/version-control.png)

### Важные ссылки 📌 - [Наше долгосрочное видение](https://github.com/usebruno/bruno/discussions/269) - [Roadmap](https://github.com/usebruno/bruno/discussions/384) - [Документация](https://docs.usebruno.com) - [Сайт](https://www.usebruno.com) - [Скачать Bruno](https://www.usebruno.com/downloads) ### Витрина 🎥 - [Отзывы](https://github.com/usebruno/bruno/discussions/343) - [Центр знаний](https://github.com/usebruno/bruno/discussions/386) - [Скриптомания](https://github.com/usebruno/bruno/discussions/385) ### Поддержка ❤️ Гав! Если вам нравится проект, нажмите на звездочку ⭐ !!! ### Поделись отзывами 📣 Если Бруно помог вам в работе и в ваших командах, пожалуйста, не забудьте поделиться своим [отзывом на нашем обсуждении в github](https://github.com/usebruno/bruno/discussions/343) ### Внести вклад 👩‍💻🧑‍💻 Я рад, что Вы хотите улучшить Бруно. Пожалуйста, ознакомьтесь с [этим гайдом](../contributing/contributing_ru.md) Даже если вы не можете внести свой вклад с помощью кода, пожалуйста, не стесняйтесь сообщать об ошибках и пожеланиях к функциям, которые необходимо реализовать для решения вашей задачи. ### Авторы
### Оставайтесь на связи 🌐 [X ( Twitter )](https://twitter.com/use_bruno)
[Наш сайт](https://www.usebruno.com)
[Discord](https://discord.com/invite/KgcZUncpjq) ### Лицензия 📄 [MIT](../../license.md) ================================================ FILE: docs/readme/readme_tr.md ================================================
### Bruno - API'leri keşfetmek ve test etmek için açık kaynaklı IDE. [![GitHub sürümü](https://badge.fury.io/gh/usebruno%2Fbruno.svg)](https://badge.fury.io/gh/usebruno%2Fbruno) [![CI](https://github.com/usebruno/bruno/actions/workflows/tests.yml/badge.svg?branch=main)](https://github.com/usebruno/bruno/actions/workflows/tests.yml) [![Commit Activity](https://img.shields.io/github/commit-activity/m/usebruno/bruno)](https://github.com/usebruno/bruno/pulse) [![X](https://img.shields.io/twitter/follow/use_bruno?style=social&logo=x)](https://twitter.com/use_bruno) [![Web Sitesi](https://img.shields.io/badge/Website-Visit-blue)](https://www.usebruno.com) [![İndir](https://img.shields.io/badge/Download-Latest-brightgreen)](https://www.usebruno.com/downloads) [English](../../readme.md) | [Українська](./readme_ua.md) | [Русский](./readme_ru.md) | **Türkçe** | [Deutsch](./readme_de.md) | [Français](./readme_fr.md) | [Português (BR)](./readme_pt_br.md) | [한국어](./readme_kr.md) | [বাংলা](./readme_bn.md) | [Español](./readme_es.md) | [Italiano](./readme_it.md) | [Română](./readme_ro.md) | [Polski](./readme_pl.md) | [简体中文](./readme_cn.md) | [正體中文](./readme_zhtw.md) | [العربية](./readme_ar.md) | [日本語](./readme_ja.md) | [ქართული](./readme_ka.md) Bruno, Postman ve benzeri araçlar tarafından temsil edilen statükoda devrim yaratmayı amaçlayan yeni ve yenilikçi bir API istemcisidir. Bruno 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. API koleksiyonlarınız üzerinde işbirliği yapmak için Git veya seçtiğiniz herhangi bir sürüm kontrolünü kullanabilirsiniz. Bruno 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) 📢 Hindistan FOSS 3.0 Konferansındaki son konuşmamızı izleyin [burada](https://www.youtube.com/watch?v=7bSMFpbcPiY) ![bruno](/assets/images/landing-2.png)

### Kurulum Bruno Mac, Windows ve Linux için ikili indirme olarak [web sitemizde](https://www.usebruno.com/downloads) mevcuttur. Bruno'yu Homebrew, Chocolatey, Scoop, Snap ve Apt gibi paket yöneticileri aracılığıyla da yükleyebilirsiniz. ```sh # Homebrew aracılığıyla Mac'te brew install bruno # Chocolatey aracılığıyla Windows'ta choco install bruno # Scoop aracılığıyla Windows'ta scoop bucket add extras scoop install bruno # Snap aracılığıyla Linux'ta snap install bruno # Apt aracılığıyla Linux'ta sudo mkdir -p /etc/apt/keyrings sudo apt update && sudo apt install gpg curl curl -fsSL "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x9FA6017ECABE0266" \ | gpg --dearmor \ | sudo tee /etc/apt/keyrings/bruno.gpg > /dev/null sudo chmod 644 /etc/apt/keyrings/bruno.gpg echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/bruno.gpg] http://debian.usebruno.com/ bruno stable" \ | sudo tee /etc/apt/sources.list.d/bruno.list sudo apt update && sudo apt install bruno ``` ### Birden fazla platformda çalıştırın 🖥️ ![bruno](/assets/images/run-anywhere.png)

### Git üzerinden katkıda bulunun 👩‍💻🧑‍💻 Veya seçtiğiniz herhangi bir sürüm kontrol sistemi ![bruno](/assets/images/version-control.png)

### Önemli Bağlantılar 📌 - [Uzun Vadeli Vizyonumuz](https://github.com/usebruno/bruno/discussions/269) - [Yol Haritası](https://github.com/usebruno/bruno/discussions/384) - [Dokümantasyon](https://docs.usebruno.com) - [Stack Overflow](https://stackoverflow.com/questions/tagged/bruno) - [Web sitesi](https://www.usebruno.com) - [Fiyatlandırma](https://www.usebruno.com/pricing) - [İndir](https://www.usebruno.com/downloads) - [GitHub Sponsorları](https://github.com/sponsors/helloanoop). ### Vitrin 🎥 - [Görüşler](https://github.com/usebruno/bruno/discussions/343) - [Bilgi Merkezi](https://github.com/usebruno/bruno/discussions/386) - [Scriptmania](https://github.com/usebruno/bruno/discussions/385) ### Destek ❤️ Bruno'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. ### Referansları Paylaşın 📣 Bruno 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. ### Yeni Paket Yöneticilerine Yayınlama Daha fazla bilgi için lütfen [buraya](../publishing/publishing_tr.md) bakın. ### Katkıda Bulunun 👩‍💻🧑‍💻 Bruno'yu geliştirmek istemenize sevindim. Lütfen [katkıda bulunma kılavuzuna](../contributing/contributing_tr.md) göz atın Kod yoluyla katkıda bulunamasanız bile, lütfen kullanım durumunuzu çözmek için uygulanması gereken hataları ve özellik isteklerini bildirmekten çekinmeyin. ### Katkıda Bulunanlar
### İletişimde Kalın 🌐 [𝕏 (Twitter)](https://twitter.com/use_bruno)
[Website](https://www.usebruno.com)
[Discord](https://discord.com/invite/KgcZUncpjq)
[LinkedIn](https://www.linkedin.com/company/usebruno) ### Ticari Marka **İsim** `Bruno` [Anoop M D](https://www.helloanoop.com/) tarafından sahip olunan bir ticari markadır. **Logo** Logo [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/) ### Lisans 📄 [MIT](../../license.md) ================================================ FILE: docs/readme/readme_ua.md ================================================
### Bruno - IDE із відкритим кодом для тестування та дослідження API [![GitHub version](https://badge.fury.io/gh/usebruno%2Fbruno.svg)](https://badge.fury.io/gh/usebruno%2Fbruno) [![CI](https://github.com/usebruno/bruno/actions/workflows/tests.yml/badge.svg?branch=main)](https://github.com/usebruno/bruno/actions/workflows/tests.yml) [![Commit Activity](https://img.shields.io/github/commit-activity/m/usebruno/bruno)](https://github.com/usebruno/bruno/pulse) [![X](https://img.shields.io/twitter/follow/use_bruno?style=social&logo=x)](https://twitter.com/use_bruno) [![Website](https://img.shields.io/badge/Website-Visit-blue)](https://www.usebruno.com) [![Download](https://img.shields.io/badge/Download-Latest-brightgreen)](https://www.usebruno.com/downloads) [English](../../readme.md) | **Українська** | [Русский](./readme_ru.md) | [Türkçe](./readme_tr.md) | [Deutsch](./readme_de.md) | [Français](./readme_fr.md) | [Português (BR)](./readme_pt_br.md) | [한국어](./readme_kr.md) | [বাংলা](./readme_bn.md) | [Español](./readme_es.md) | [Italiano](./readme_it.md) | [Română](./readme_ro.md) | [Polski](./readme_pl.md) | [简体中文](./readme_cn.md) | [正體中文](./readme_zhtw.md) | [العربية](./readme_ar.md) | [日本語](./readme_ja.md) | [ქართული](./readme_ka.md) Bruno це новий та іноваційний API клієнт, націлений на революційну зміну статусy кво, запровадженого інструментами на кшталт Postman. Bruno зберігає ваші колекції напряму у теці на вашому диску. Він використовує текстову мову розмітки Bru для збереження інформації про ваші API запити. Ви можете використовувати git або будь-яку іншу систему контролю версій щоб спільно працювати над вашими колекціями API запитів. Bruno є повністю автономним. Немає жодних планів додавати будь-які синхронізації через хмару, ніколи. Ми цінуємо приватність ваших даних, і вважаєм, що вони мають залишитись лише на вашому комп'ютері. Дізнатись більше про наше бачення у довготривалій перспективі можна [тут](https://github.com/usebruno/bruno/discussions/269) ![bruno](/assets/images/landing-2.png)

### Кросплатформенність 🖥️ ![bruno](/assets/images/run-anywhere.png)

### Спільна робота через Git 👩‍💻🧑‍💻 Або будь-яку іншу систему контролю версій на ваш вибір ![bruno](/assets/images/version-control.png)

### Важливі посилання 📌 - [Наше бачення довготривалої перспективи проекту](https://github.com/usebruno/bruno/discussions/269) - [Дорожня карта проекту](https://github.com/usebruno/bruno/discussions/384) - [Документація](https://docs.usebruno.com) - [Сайт](https://www.usebruno.com) - [Завантаження](https://www.usebruno.com/downloads) ### Вітрина 🎥 - [Відгуки](https://github.com/usebruno/bruno/discussions/343) - [Хаб знань](https://github.com/usebruno/bruno/discussions/386) - [Scriptmania](https://github.com/usebruno/bruno/discussions/385) ### Підтримка ❤️ Гав! Якщо вам сподобався проект, тисніть на ⭐ !! ### Поділитись відгуками 📣 Якщо Bruno допоміг у роботі вам або вашій команді, будь ласка не забудьте поділитись вашими [відгуками у github дискусії](https://github.com/usebruno/bruno/discussions/343) ### Зробити свій внесок 👩‍💻🧑‍💻 Я радий що ви бажаєте покращити Bruno. Будь ласка переглянте [інструкцію по контрибуції](../contributing/contributing_ua.md) Навіть якщо ви не можете зробити свій внесок пишучи код, будь ласка не соромтесь рапортувати про помилки і писати запити на новий функціонал, який потрібен вам у вашій роботі. ### Автори
### Залишайтесь на зв'язку 🌐 [Twitter](https://twitter.com/use_bruno)
[Сайт](https://www.usebruno.com)
[Discord](https://discord.com/invite/KgcZUncpjq)
[LinkedIn](https://www.linkedin.com/company/usebruno) ### Ліцензія 📄 [MIT](../../license.md) ================================================ FILE: docs/readme/readme_zhtw.md ================================================
### Bruno - 探索和測試 API 的開源 IDE 工具 [![GitHub version](https://badge.fury.io/gh/usebruno%2Fbruno.svg)](https://badge.fury.io/gh/usebruno%2Fbruno) [![CI](https://github.com/usebruno/bruno/actions/workflows/tests.yml/badge.svg?branch=main)](https://github.com/usebruno/bruno/actions/workflows/tests.yml) [![Commit Activity](https://img.shields.io/github/commit-activity/m/usebruno/bruno)](https://github.com/usebruno/bruno/pulse) [![X](https://img.shields.io/twitter/follow/use_bruno?style=social&logo=x)](https://twitter.com/use_bruno) [![网站](https://img.shields.io/badge/Website-Visit-blue)](https://www.usebruno.com) [![下载](https://img.shields.io/badge/Download-Latest-brightgreen)](https://www.usebruno.com/downloads) [English](../../readme.md) | [Українська](./readme_ua.md) | [Русский](./readme_ru.md) | [Türkçe](./readme_tr.md) | [Deutsch](./readme_de.md) | [Français](./readme_fr.md) | [Português (BR)](./readme_pt_br.md) | [한국어](./readme_kr.md) | [বাংলা](./readme_bn.md) | [Español](./readme_es.md) | [Italiano](./readme_it.md) | [Română](./readme_ro.md) | [Polski](./readme_pl.md) | [简体中文](./readme_cn.md) | **正體中文** | [العربية](./readme_ar.md) | [日本語](./readme_ja.md) | [ქართული](./readme_ka.md) Bruno 是一個全新且有創新性的 API 用戶端,目的在徹底改變以 Postman 和其他類似工具的現況。 Bruno 將您的 API 集合直接儲存在檔案系統上的資料夾中。我們以純文本標記語言- Bru,來儲存和 API 有關的資訊。 您可以使用 Git 或您選擇的任何版本管理軟體,來管理及協作 API 集合。 Bruno 僅能夠離線使用,永遠不會計劃為 Bruno 增加雲端同步的功能。我們重視您的資料隱私,並相信它應該保留在您的裝置上。瞭解我們的長期願景 [連結](https://github.com/usebruno/bruno/discussions/269) 📢 觀看我們最近在 India FOSS 3.0 研討會上的演講 [連結](https://www.youtube.com/watch?v=7bSMFpbcPiY) ![bruno](../../assets/images/landing-2.png)

### 安装 可以在我們的 [網站上下載](https://www.usebruno.com/downloads) 跨平臺(Mac、Windows 和 Linux)的 Bruno 程式檔。 您也可以透過套件管理程式來安裝 Bruno,如:Homebrew、Chocolatey、Scoop、Snap 和 Apt。 ```shell # 在 Mac 上使用 Homebrew 安裝 brew install bruno # 在 Windows 上使用 Chocolatey 安裝 choco install bruno # 在 Windows 上使用 Scoop 安裝 scoop bucket add extras scoop install bruno # 在 Linux 上使用 Snap 安裝 snap install bruno # 在 Linux 上使用 Apt 安裝 sudo mkdir -p /etc/apt/keyrings sudo apt update && sudo apt install gpg curl curl -fsSL "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x9FA6017ECABE0266" \ | gpg --dearmor \ | sudo tee /etc/apt/keyrings/bruno.gpg > /dev/null sudo chmod 644 /etc/apt/keyrings/bruno.gpg echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/bruno.gpg] http://debian.usebruno.com/ bruno stable" \ | sudo tee /etc/apt/sources.list.d/bruno.list sudo apt update && sudo apt install bruno ``` ### 跨多個平台運行 🖥️ ![bruno](../../assets/images/run-anywhere.png)

### 透過 Git 進行協作 👩‍💻🧑‍💻 您選擇的任何版本管理軟體 ![bruno](../../assets/images/version-control.png)

### 重要連結 📌 - [我們的長期願景](https://github.com/usebruno/bruno/discussions/269) - [藍圖](https://github.com/usebruno/bruno/discussions/384) - [說明文件](https://docs.usebruno.com) - [Stack Overflow](https://stackoverflow.com/questions/tagged/bruno) - [網站](https://www.usebruno.com) - [定價](https://www.usebruno.com/pricing) - [下載](https://www.usebruno.com/downloads) - [GitHub 贊助](https://github.com/sponsors/helloanoop). ### 展示 🎥 - [Testimonials](https://github.com/usebruno/bruno/discussions/343) - [Knowledge Hub](https://github.com/usebruno/bruno/discussions/386) - [Scriptmania](https://github.com/usebruno/bruno/discussions/385) ### 贊助支持 ❤️ 如果您喜歡 Bruno 和希望支持我們在開源上的工作,請考慮使用 [GitHub Sponsors](https://github.com/sponsors/helloanoop) 來贊助我們。 ### 分享感想 📣 如果 Bruno 在工作和您的團隊中為您提供了幫助,請不要忘記在我們的 [GitHub 討論區](https://github.com/usebruno/bruno/discussions/343) 中分享您的感想。 ### 發佈到新的套件管理器 更多資訊,請參考這個 [連結](../publishing/publishing_zhtw.md) 。 ### 持續關注 🌐 [𝕏 (Twitter)](https://twitter.com/use_bruno)
[Website](https://www.usebruno.com)
[Discord](https://discord.com/invite/KgcZUncpjq)
[LinkedIn](https://www.linkedin.com/company/usebruno) ### 商標 **名稱** `Bruno` 是 [Anoop M D](https://www.helloanoop.com/) 持有的商標。 **Logo** Logo 源自於 [OpenMoji](https://openmoji.org/library/emoji-1F436/)。授權: CC [BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/) ### 提供貢獻 👩‍💻🧑‍💻 我很高興您希望一同改善 Bruno。請參考 [貢獻指南](../contributing/contributing_zhtw.md)。 即使您無法透過程式碼做出貢獻,我們仍然歡迎您提出 Bug 及新的實作需求。 ### 作者們
### 授權許可 📄 [MIT](../../license.md) ================================================ FILE: eslint.config.js ================================================ // eslint.config.js const { defineConfig } = require('eslint/config'); const globals = require('globals'); const { fixupPluginRules } = require('@eslint/compat'); const eslintPluginDiff = require('eslint-plugin-diff'); let stylistic; const runESMImports = async () => { stylistic = await import('@stylistic/eslint-plugin').then((d) => d.default); }; module.exports = runESMImports().then(() => defineConfig([ // Global ignores - must be a standalone object with ONLY ignores { ignores: [ '**/node_modules/**/*', '**/dist/**/*', '**/*.bru', 'packages/bruno-js/src/sandbox/bundle-browser-rollup.js', 'packages/bruno-app/public/static/**/*', 'packages/bruno-app/.next/**/*', 'packages/bruno-electron/web/**/*' ] }, { plugins: { 'diff': fixupPluginRules(eslintPluginDiff), '@stylistic': stylistic }, languageOptions: { parser: require('@typescript-eslint/parser'), parserOptions: { ecmaVersion: 'latest', sourceType: 'module' } }, files: [ './eslint.config.js', 'tests/**/*.{ts,js}', 'playwright/**/*.{js,ts}', 'packages/bruno-app/**/*.{js,jsx,ts}', 'packages/bruno-app/src/test-utils/mocks/codemirror.js', 'packages/bruno-cli/**/*.js', 'packages/bruno-common/**/*.ts', 'packages/bruno-converters/**/*.js', 'packages/bruno-electron/**/*.js', 'packages/bruno-filestore/**/*.ts', 'packages/bruno-schema-types/**/*.ts', 'packages/bruno-js/**/*.js', 'packages/bruno-lang/**/*.js', 'packages/bruno-requests/**/*.ts', 'packages/bruno-requests/**/*.js', 'packages/bruno-tests/**/*.{js,ts}' ], rules: { ...stylistic.configs.customize({ indent: 2, quotes: 'single', semi: true, jsx: true }).rules, '@stylistic/comma-dangle': ['error', 'never'], '@stylistic/brace-style': ['error', '1tbs', { allowSingleLine: true }], '@stylistic/arrow-parens': ['error', 'always'], '@stylistic/curly-newline': ['error', { multiline: true, minElements: 2, consistent: true }], '@stylistic/function-paren-newline': ['off'], '@stylistic/array-bracket-spacing': ['error', 'never'], '@stylistic/arrow-spacing': ['error', { before: true, after: true }], '@stylistic/function-call-spacing': ['error', 'never'], '@stylistic/multiline-ternary': ['off'], '@stylistic/padding-line-between-statements': ['off'], '@stylistic/semi-style': ['error', 'last'], '@stylistic/max-len': ['off'], '@stylistic/jsx-one-expression-per-line': ['off'], '@stylistic/max-statements-per-line': ['off'], '@stylistic/no-mixed-operators': ['off'] } }, { files: ['packages/bruno-app/**/*.{js,jsx,ts}'], ignores: ['**/*.config.js', '**/public/**/*'], languageOptions: { globals: { ...globals.browser, ...globals.jest, global: false, require: false, Buffer: false, process: false, ipcRenderer: false }, parserOptions: { ecmaFeatures: { jsx: true } } }, rules: { 'no-undef': 'error' } }, { // It prevents lint errors when using CommonJS exports (module.exports) in Jest mocks. files: ['packages/bruno-app/src/test-utils/mocks/codemirror.js'], languageOptions: { globals: { ...globals.node, ...globals.jest } }, rules: { 'no-undef': 'error' } }, { // Storybook config files use CommonJS with __dirname and module.exports files: ['packages/bruno-app/storybook/**/*.js'], languageOptions: { globals: { ...globals.node } }, rules: { 'no-undef': 'error' } }, { files: ['packages/bruno-cli/**/*.js'], ignores: ['**/*.config.js'], languageOptions: { globals: { ...globals.node, ...globals.jest }, parserOptions: { ecmaVersion: 'latest' } }, rules: { 'no-undef': 'error' } }, { files: ['packages/bruno-common/**/*.ts'], ignores: ['**/*.config.js', '**/dist/**/*'], languageOptions: { globals: { ...globals.node, ...globals.jest }, parser: require('@typescript-eslint/parser'), parserOptions: { ecmaVersion: 'latest', sourceType: 'module', project: './packages/bruno-common/tsconfig.json' } }, rules: { 'no-undef': 'error' } }, { files: ['packages/bruno-converters/**/*.js'], ignores: ['**/*.config.js', '**/dist/**/*'], languageOptions: { globals: { ...globals.node, ...globals.jest }, parserOptions: { ecmaVersion: 'latest', sourceType: 'module' } }, rules: { 'no-undef': 'error' } }, { files: ['packages/bruno-electron/**/*.js'], ignores: ['**/*.config.js', '**/web/**/*'], languageOptions: { globals: { ...globals.node, ...globals.jest } }, rules: { 'no-undef': 'error' } }, { files: ['packages/bruno-filestore/**/*.ts'], ignores: ['**/*.config.js', '**/dist/**/*'], languageOptions: { globals: { ...globals.node, ...globals.jest }, parser: require('@typescript-eslint/parser'), parserOptions: { ecmaVersion: 'latest', sourceType: 'module', project: './packages/bruno-filestore/tsconfig.json' } }, rules: { 'no-undef': 'error' } }, { files: ['packages/bruno-js/**/*.js'], ignores: ['**/*.config.js', '**/dist/**/*'], languageOptions: { globals: { ...globals.node, ...globals.jest, window: false, self: false, HTMLElement: false, typeDetectGlobalObject: false }, parserOptions: { ecmaVersion: 'latest', sourceType: 'module' } }, rules: { 'no-undef': 'error' } }, { files: ['packages/bruno-lang/**/*.js'], ignores: ['**/*.config.js', '**/dist/**/*'], languageOptions: { globals: { ...globals.node, ...globals.jest }, parserOptions: { ecmaVersion: 'latest', sourceType: 'module' } }, rules: { 'no-undef': 'error' } }, { files: ['packages/bruno-requests/**/*.ts'], ignores: ['**/*.config.js', '**/dist/**/*'], languageOptions: { globals: { ...globals.node, ...globals.jest }, parser: require('@typescript-eslint/parser'), parserOptions: { ecmaVersion: 'latest', sourceType: 'module', project: './packages/bruno-requests/tsconfig.json' } }, rules: { 'no-undef': 'error' } }, { files: ['packages/bruno-requests/**/*.js'], ignores: ['**/*.config.js', '**/dist/**/*'], languageOptions: { globals: { ...globals.node, ...globals.jest }, parserOptions: { ecmaVersion: 'latest', sourceType: 'module' } }, rules: { 'no-undef': 'error' } } ])); ================================================ FILE: license.md ================================================ MIT License Copyright (c) 2022 Anoop M D, Anusree P S and Contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: package.json ================================================ { "name": "usebruno", "private": true, "workspaces": [ "packages/bruno-app", "packages/bruno-electron", "packages/bruno-cli", "packages/bruno-common", "packages/bruno-converters", "packages/bruno-schema", "packages/bruno-schema-types", "packages/bruno-query", "packages/bruno-js", "packages/bruno-lang", "packages/bruno-tests", "packages/bruno-toml", "packages/bruno-graphql-docs", "packages/bruno-requests", "packages/bruno-filestore" ], "homepage": "https://usebruno.com", "devDependencies": { "@eslint/compat": "^1.3.2", "@faker-js/faker": "^7.6.0", "@jest/globals": "^29.2.0", "@opencollection/types": "~0.8.0", "@playwright/test": "^1.51.1", "@rollup/plugin-json": "^6.1.0", "@storybook/addon-webpack5-compiler-babel": "^4.0.0", "@storybook/builder-webpack5": "^10.1.10", "@storybook/react": "^10.1.10", "@storybook/react-webpack5": "^10.1.10", "@stylistic/eslint-plugin": "^5.3.1", "@types/jest": "^29.5.11", "@types/lodash-es": "^4.17.12", "@types/node": "^22.14.1", "@typescript-eslint/parser": "^8.39.0", "concurrently": "^8.2.2", "cross-env": "10.1.0", "eslint": "^9.26.0", "eslint-plugin-diff": "^2.0.3", "fs-extra": "^11.1.1", "globals": "^16.1.0", "husky": "^9.1.7", "jest": "^29.2.0", "lodash-es": "^4.17.21", "nano-staged": "^0.8.0", "playwright": "^1.51.1", "pretty-quick": "^3.1.3", "randomstring": "^1.2.2", "rimraf": "^6.0.1", "storybook": "^10.1.10", "ts-jest": "^29.2.6" }, "scripts": { "setup": "node ./scripts/setup.js", "watch:converters": "npm run watch --workspace=packages/bruno-converters", "dev": "node ./scripts/dev.js", "watch": "npm run dev:watch", "dev:watch": "node ./scripts/dev-hot-reload.js", "dev:web": "npm run dev --workspace=packages/bruno-app", "build:web": "npm run build --workspace=packages/bruno-app", "dev:electron": "npm run dev --workspace=packages/bruno-electron", "dev:electron:debug": "npm run debug --workspace=packages/bruno-electron", "storybook": "npm run storybook --workspace=packages/bruno-app", "build:bruno-common": "npm run build --workspace=packages/bruno-common", "build:bruno-requests": "npm run build --workspace=packages/bruno-requests", "build:bruno-filestore": "npm run build --workspace=packages/bruno-filestore", "build:bruno-converters": "npm run build --workspace=packages/bruno-converters", "build:bruno-query": "npm run build --workspace=packages/bruno-query", "build:graphql-docs": "npm run build --workspace=packages/bruno-graphql-docs", "build:schema-types": "npm run build --workspace=packages/bruno-schema-types", "build:electron": "node ./scripts/build-electron.js", "build:electron:mac": "./scripts/build-electron.sh mac", "build:electron:win": "./scripts/build-electron.sh win", "build:electron:linux": "./scripts/build-electron.sh linux", "build:electron:deb": "./scripts/build-electron.sh deb", "build:electron:rpm": "./scripts/build-electron.sh rpm", "build:electron:snap": "./scripts/build-electron.sh snap", "watch:common": "npm run watch --workspace=packages/bruno-common", "watch:requests": "npm run watch --workspace=packages/bruno-requests", "test:codegen": "node playwright/codegen.ts", "test:e2e": "playwright test --project=default", "test:e2e:ssl": "playwright test --project=ssl", "lint": "cross-env NODE_OPTIONS=\"--max_old_space_size=4096\" npx eslint", "lint:fix": "cross-env NODE_OPTIONS=\"--max_old_space_size=4096\" npx eslint --fix", "prepare": "husky" }, "nano-staged": { "*.{js,ts,jsx}": [ "npm run lint:fix" ] }, "overrides": { "rollup": "3.29.5", "electron-store": { "conf": { "json-schema-typed": "8.0.1" } } }, "dependencies": { "ajv": "^8.17.1", "git-url-parse": "^14.1.0" } } ================================================ FILE: packages/bruno-app/.babelrc ================================================ { "presets": ["@babel/preset-env", "@babel/preset-react"], "plugins": [["styled-components", { "ssr": true }]] } ================================================ FILE: packages/bruno-app/.gitignore ================================================ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. # dependencies node_modules .pnp .pnp.js pnpm-lock.yaml package-lock.json yarn.lock # testing coverage # production build # misc .DS_Store *.pem # debug npm-debug.log* yarn-debug.log* yarn-error.log* *.log # local env files .env.local .env.development.local .env.test.local .env.production.local # next.js .next/ dist/ .env storybook-static/ ================================================ FILE: packages/bruno-app/babel.config.js ================================================ module.exports = { presets: [ '@babel/preset-env', ['@babel/preset-react', { runtime: 'automatic' }] ], plugins: ['babel-plugin-styled-components'] }; ================================================ FILE: packages/bruno-app/jest.config.js ================================================ module.exports = { rootDir: '.', transform: { '^.+\\.[jt]sx?$': 'babel-jest' }, transformIgnorePatterns: [ '/node_modules/(?!strip-json-comments|nanoid|xml-formatter)/' ], moduleNameMapper: { '^assets/(.*)$': '/src/assets/$1', '^components/(.*)$': '/src/components/$1', '^hooks/(.*)$': '/src/hooks/$1', '^themes/(.*)$': '/src/themes/$1', '^api/(.*)$': '/src/api/$1', '^pageComponents/(.*)$': '/src/pageComponents/$1', '^providers/(.*)$': '/src/providers/$1', '^utils/(.*)$': '/src/utils/$1', '^test-utils/(.*)$': '/src/test-utils/$1' }, clearMocks: true, moduleDirectories: ['node_modules', 'src'], testEnvironment: 'jsdom', setupFilesAfterEnv: ['@testing-library/jest-dom'], setupFiles: [ '/jest.setup.js' ], testMatch: [ '/src/**/*.spec.[jt]s?(x)' ] }; ================================================ FILE: packages/bruno-app/jest.setup.js ================================================ jest.mock('nanoid', () => { return { nanoid: () => {} }; }); jest.mock('strip-json-comments', () => { return { stripJsonComments: (str) => str }; }); ================================================ FILE: packages/bruno-app/jsconfig.json ================================================ { "compilerOptions": { "jsx": "react", "target": "es2017", "allowSyntheticDefaultImports": false, "baseUrl": "./", "paths": { "assets/*": ["src/assets/*"], "ui/*": ["src/ui/*"], "components/*": ["src/components/*"], "hooks/*": ["src/hooks/*"], "themes/*": ["src/themes/*"], "api/*": ["src/api/*"], "pageComponents/*": ["src/pageComponents/*"], "providers/*": ["src/providers/*"], "utils/*": ["src/utils/*"] } }, "exclude": ["node_modules", "dist"] } ================================================ FILE: packages/bruno-app/package.json ================================================ { "name": "@usebruno/app", "version": "2.0.0", "license": "MIT", "private": true, "scripts": { "dev": "rsbuild dev", "build": "rsbuild build -m production", "preview": "rsbuild preview", "test": "jest", "storybook": "storybook dev -p 6006 --config-dir storybook", "build-storybook": "storybook build --config-dir storybook" }, "dependencies": { "@fontsource/inter": "^5.0.15", "@prantlf/jsonlint": "^16.0.0", "@reduxjs/toolkit": "^1.8.0", "@tabler/icons": "^1.46.0", "@testing-library/user-event": "^14.6.1", "@tippyjs/react": "^4.2.6", "@usebruno/common": "0.1.0", "@usebruno/graphql-docs": "0.1.0", "@usebruno/schema": "0.7.0", "@xterm/addon-fit": "^0.10.0", "@xterm/xterm": "^5.5.0", "classnames": "^2.3.1", "codemirror": "5.65.2", "codemirror-graphql": "2.1.1", "cookie": "0.7.1", "diff2html": "^3.4.47", "dompurify": "^3.2.4", "escape-html": "^1.0.3", "fast-fuzzy": "^1.12.0", "fast-json-format": "~0.4.0", "file": "^0.2.2", "file-dialog": "^0.0.8", "file-saver": "^2.0.5", "formik": "^2.2.9", "github-markdown-css": "^5.2.0", "graphiql": "3.7.1", "graphql": "^16.6.0", "graphql-request": "^3.7.0", "hexy": "^0.3.5", "httpsnippet": "^3.0.9", "i18next": "24.1.2", "idb": "^7.0.0", "immer": "^9.0.15", "js-yaml": "^4.1.0", "jsesc": "^3.0.2", "jshint": "^2.13.6", "json5": "^2.2.3", "jsonc-parser": "^3.2.1", "jsonpath-plus": "^10.3.0", "jsonschema": "^1.5.0", "know-your-http-well": "^0.5.0", "linkify-it": "^5.0.0", "lodash": "^4.17.21", "markdown-it": "^13.0.2", "markdown-it-replace-link": "^1.2.0", "mime-types": "^3.0.2", "moment": "^2.30.1", "moment-timezone": "^0.5.47", "mousetrap": "^1.6.5", "nanoid": "3.3.8", "path": "^0.12.7", "pdfjs-dist": "4.4.168", "platform": "^1.3.6", "polished": "^4.3.1", "posthog-node": "4.2.1", "prettier": "^2.7.1", "qs": "^6.14.1", "query-string": "^7.0.1", "react": "19.0.0", "react-copy-to-clipboard": "^5.1.0", "react-dnd": "^16.0.1", "react-dnd-html5-backend": "^16.0.1", "react-dom": "19.0.0", "react-hot-toast": "^2.4.0", "react-i18next": "^15.0.1", "react-inspector": "^6.0.2", "react-json-view": "^1.21.3", "react-pdf": "9.1.1", "react-player": "^2.16.0", "react-redux": "^7.2.9", "react-tooltip": "^5.5.2", "react-virtuoso": "^4.18.1", "sass": "^1.46.0", "semver": "^7.7.1", "shell-quote": "^1.8.3", "strip-json-comments": "^5.0.1", "styled-components": "^5.3.3", "swagger-ui-react": "^5.31.0", "system": "^2.0.1", "url": "^0.11.3", "xml-formatter": "^3.5.0", "xml2js": "^0.6.2", "yup": "^0.32.11" }, "devDependencies": { "@babel/core": "^7.27.1", "@babel/preset-env": "^7.27.2", "@babel/preset-react": "^7.27.1", "@rsbuild/core": "^1.1.2", "@rsbuild/plugin-babel": "^1.0.3", "@rsbuild/plugin-node-polyfill": "^1.2.0", "@rsbuild/plugin-react": "^1.0.7", "@rsbuild/plugin-sass": "^1.1.0", "@rsbuild/plugin-styled-components": "1.1.0", "@testing-library/dom": "^10.4.1", "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^16.3.0", "autoprefixer": "10.4.20", "babel-jest": "^29.7.0", "babel-plugin-react-compiler": "19.0.0-beta-a7bf2bd-20241110", "babel-plugin-styled-components": "^2.1.4", "cross-env": "^7.0.3", "css-loader": "7.1.2", "file-loader": "^6.2.0", "html-loader": "^3.0.1", "html-webpack-plugin": "^5.5.0", "jest-environment-jsdom": "^29.7.0", "mini-css-extract-plugin": "^2.4.5", "postcss": "8.4.47", "style-loader": "^3.3.1", "tailwindcss": "^3.4.1", "webpack": "^5.64.4", "webpack-cli": "^4.9.1" }, "overrides": { "httpsnippet": { "form-data": "4.0.4" } } } ================================================ FILE: packages/bruno-app/postcss.config.js ================================================ module.exports = { plugins: { tailwindcss: {}, autoprefixer: {} } }; ================================================ FILE: packages/bruno-app/public/static/diff2Html.js ================================================ !(function (e, t) { 'object' == typeof exports && 'object' == typeof module ? (module.exports = t()) : 'function' == typeof define && define.amd ? define('Diff2Html', [], t) : 'object' == typeof exports ? (exports.Diff2Html = t()) : (e.Diff2Html = t()); })(this, () => { return ( (e = { 696: (e, t) => { 'use strict'; Object.defineProperty(t, '__esModule', { value: !0 }), (t.convertChangesToDMP = function (e) { for (var t, n, i = [], r = 0; r < e.length; r++) (n = (t = e[r]).added ? 1 : t.removed ? -1 : 0), i.push([n, t.value]); return i; }); }, 826: (e, t) => { 'use strict'; Object.defineProperty(t, '__esModule', { value: !0 }), (t.convertChangesToXML = function (e) { for (var t = [], n = 0; n < e.length; n++) { var i = e[n]; i.added ? t.push('') : i.removed && t.push(''), t.push( i.value.replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"') ), i.added ? t.push('') : i.removed && t.push(''); } return t.join(''); }); }, 976: (e, t, n) => { 'use strict'; var i; Object.defineProperty(t, '__esModule', { value: !0 }), (t.diffArrays = function (e, t, n) { return r.diff(e, t, n); }), (t.arrayDiff = void 0); var r = new ((i = n(913)) && i.__esModule ? i : { default: i }).default(); (t.arrayDiff = r), (r.tokenize = function (e) { return e.slice(); }), (r.join = r.removeEmpty = function (e) { return e; }); }, 913: (e, t) => { 'use strict'; function n() {} function i(e, t, n, i, r) { for (var s = 0, o = t.length, a = 0, l = 0; s < o; s++) { var c = t[s]; if (c.removed) { if (((c.value = e.join(i.slice(l, l + c.count))), (l += c.count), s && t[s - 1].added)) { var d = t[s - 1]; (t[s - 1] = t[s]), (t[s] = d); } } else { if (!c.added && r) { var f = n.slice(a, a + c.count); (f = f.map(function (e, t) { var n = i[l + t]; return n.length > e.length ? n : e; })), (c.value = e.join(f)); } else c.value = e.join(n.slice(a, a + c.count)); (a += c.count), c.added || (l += c.count); } } var u = t[o - 1]; return ( o > 1 && 'string' == typeof u.value && (u.added || u.removed) && e.equals('', u.value) && ((t[o - 2].value += u.value), t.pop()), t ); } Object.defineProperty(t, '__esModule', { value: !0 }), (t.default = n), (n.prototype = { diff: function (e, t) { var n = arguments.length > 2 && void 0 !== arguments[2] ? arguments[2] : {}, r = n.callback; 'function' == typeof n && ((r = n), (n = {})), (this.options = n); var s = this; function o(e) { return r ? (setTimeout(function () { r(void 0, e); }, 0), !0) : e; } (e = this.castInput(e)), (t = this.castInput(t)), (e = this.removeEmpty(this.tokenize(e))); var a = (t = this.removeEmpty(this.tokenize(t))).length, l = e.length, c = 1, d = a + l; n.maxEditLength && (d = Math.min(d, n.maxEditLength)); var f = [{ newPos: -1, components: [] }], u = this.extractCommon(f[0], t, e, 0); if (f[0].newPos + 1 >= a && u + 1 >= l) return o([{ value: this.join(t), count: t.length }]); function h() { for (var n = -1 * c; n <= c; n += 2) { var r = void 0, d = f[n - 1], u = f[n + 1], h = (u ? u.newPos : 0) - n; d && (f[n - 1] = void 0); var p = d && d.newPos + 1 < a, b = u && 0 <= h && h < l; if (p || b) { if ( (!p || (b && d.newPos < u.newPos) ? ((r = { newPos: (g = u).newPos, components: g.components.slice(0) }), s.pushComponent(r.components, void 0, !0)) : ((r = d).newPos++, s.pushComponent(r.components, !0, void 0)), (h = s.extractCommon(r, t, e, n)), r.newPos + 1 >= a && h + 1 >= l) ) return o(i(s, r.components, t, e, s.useLongestToken)); f[n] = r; } else f[n] = void 0; } var g; c++; } if (r) !(function e() { setTimeout(function () { if (c > d) return r(); h() || e(); }, 0); })(); else for (; c <= d; ) { var p = h(); if (p) return p; } }, pushComponent: function (e, t, n) { var i = e[e.length - 1]; i && i.added === t && i.removed === n ? (e[e.length - 1] = { count: i.count + 1, added: t, removed: n }) : e.push({ count: 1, added: t, removed: n }); }, extractCommon: function (e, t, n, i) { for ( var r = t.length, s = n.length, o = e.newPos, a = o - i, l = 0; o + 1 < r && a + 1 < s && this.equals(t[o + 1], n[a + 1]); ) o++, a++, l++; return l && e.components.push({ count: l }), (e.newPos = o), a; }, equals: function (e, t) { return this.options.comparator ? this.options.comparator(e, t) : e === t || (this.options.ignoreCase && e.toLowerCase() === t.toLowerCase()); }, removeEmpty: function (e) { for (var t = [], n = 0; n < e.length; n++) e[n] && t.push(e[n]); return t; }, castInput: function (e) { return e; }, tokenize: function (e) { return e.split(''); }, join: function (e) { return e.join(''); } }); }, 630: (e, t, n) => { 'use strict'; var i; Object.defineProperty(t, '__esModule', { value: !0 }), (t.diffChars = function (e, t, n) { return r.diff(e, t, n); }), (t.characterDiff = void 0); var r = new ((i = n(913)) && i.__esModule ? i : { default: i }).default(); t.characterDiff = r; }, 852: (e, t, n) => { 'use strict'; var i; Object.defineProperty(t, '__esModule', { value: !0 }), (t.diffCss = function (e, t, n) { return r.diff(e, t, n); }), (t.cssDiff = void 0); var r = new ((i = n(913)) && i.__esModule ? i : { default: i }).default(); (t.cssDiff = r), (r.tokenize = function (e) { return e.split(/([{}:;,]|\s+)/); }); }, 276: (e, t, n) => { 'use strict'; Object.defineProperty(t, '__esModule', { value: !0 }), (t.diffJson = function (e, t, n) { return l.diff(e, t, n); }), (t.canonicalize = c), (t.jsonDiff = void 0); var i, r = (i = n(913)) && i.__esModule ? i : { default: i }, s = n(187); function o(e) { return ( (o = 'function' == typeof Symbol && 'symbol' == typeof Symbol.iterator ? function (e) { return typeof e; } : function (e) { return e && 'function' == typeof Symbol && e.constructor === Symbol && e !== Symbol.prototype ? 'symbol' : typeof e; }), o(e) ); } var a = Object.prototype.toString, l = new r.default(); function c(e, t, n, i, r) { var s, l; for (t = t || [], n = n || [], i && (e = i(r, e)), s = 0; s < t.length; s += 1) if (t[s] === e) return n[s]; if ('[object Array]' === a.call(e)) { 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); return t.pop(), n.pop(), l; } if ((e && e.toJSON && (e = e.toJSON()), 'object' === o(e) && null !== e)) { t.push(e), (l = {}), n.push(l); var d, f = []; for (d in e) e.hasOwnProperty(d) && f.push(d); for (f.sort(), s = 0; s < f.length; s += 1) l[(d = f[s])] = c(e[d], t, n, i, d); t.pop(), n.pop(); } else l = e; return l; } (t.jsonDiff = l), (l.useLongestToken = !0), (l.tokenize = s.lineDiff.tokenize), (l.castInput = function (e) { var t = this.options, n = t.undefinedReplacement, i = t.stringifyReplacer, r = void 0 === i ? function (e, t) { return void 0 === t ? n : t; } : i; return 'string' == typeof e ? e : JSON.stringify(c(e, null, null, r), r, ' '); }), (l.equals = function (e, t) { return r.default.prototype.equals.call(l, e.replace(/,([\r\n])/g, '$1'), t.replace(/,([\r\n])/g, '$1')); }); }, 187: (e, t, n) => { 'use strict'; Object.defineProperty(t, '__esModule', { value: !0 }), (t.diffLines = function (e, t, n) { return o.diff(e, t, n); }), (t.diffTrimmedLines = function (e, t, n) { var i = (0, s.generateOptions)(n, { ignoreWhitespace: !0 }); return o.diff(e, t, i); }), (t.lineDiff = void 0); var i, r = (i = n(913)) && i.__esModule ? i : { default: i }, s = n(9), o = new r.default(); (t.lineDiff = o), (o.tokenize = function (e) { var t = [], n = e.split(/(\n|\r\n)/); n[n.length - 1] || n.pop(); for (var i = 0; i < n.length; i++) { var r = n[i]; i % 2 && !this.options.newlineIsToken ? (t[t.length - 1] += r) : (this.options.ignoreWhitespace && (r = r.trim()), t.push(r)); } return t; }); }, 146: (e, t, n) => { 'use strict'; var i; Object.defineProperty(t, '__esModule', { value: !0 }), (t.diffSentences = function (e, t, n) { return r.diff(e, t, n); }), (t.sentenceDiff = void 0); var r = new ((i = n(913)) && i.__esModule ? i : { default: i }).default(); (t.sentenceDiff = r), (r.tokenize = function (e) { return e.split(/(\S.+?[.!?])(?=\s+|$)/); }); }, 303: (e, t, n) => { 'use strict'; Object.defineProperty(t, '__esModule', { value: !0 }), (t.diffWords = function (e, t, n) { return (n = (0, s.generateOptions)(n, { ignoreWhitespace: !0 })), l.diff(e, t, n); }), (t.diffWordsWithSpace = function (e, t, n) { return l.diff(e, t, n); }), (t.wordDiff = void 0); var i, r = (i = n(913)) && i.__esModule ? i : { default: i }, s = n(9), o = /^[A-Za-z\xC0-\u02C6\u02C8-\u02D7\u02DE-\u02FF\u1E00-\u1EFF]+$/, a = /\S/, l = new r.default(); (t.wordDiff = l), (l.equals = function (e, t) { return ( this.options.ignoreCase && ((e = e.toLowerCase()), (t = t.toLowerCase())), e === t || (this.options.ignoreWhitespace && !a.test(e) && !a.test(t)) ); }), (l.tokenize = function (e) { for (var t = e.split(/([^\S\r\n]+|[()[\]{}'"\r\n]|\b)/), n = 0; n < t.length - 1; n++) !t[n + 1] && t[n + 2] && o.test(t[n]) && o.test(t[n + 2]) && ((t[n] += t[n + 2]), t.splice(n + 1, 2), n--); return t; }); }, 785: (e, t, n) => { 'use strict'; Object.defineProperty(t, '__esModule', { value: !0 }), Object.defineProperty(t, 'Diff', { enumerable: !0, get: function () { return r.default; } }), Object.defineProperty(t, 'diffChars', { enumerable: !0, get: function () { return s.diffChars; } }), Object.defineProperty(t, 'diffWords', { enumerable: !0, get: function () { return o.diffWords; } }), Object.defineProperty(t, 'diffWordsWithSpace', { enumerable: !0, get: function () { return o.diffWordsWithSpace; } }), Object.defineProperty(t, 'diffLines', { enumerable: !0, get: function () { return a.diffLines; } }), Object.defineProperty(t, 'diffTrimmedLines', { enumerable: !0, get: function () { return a.diffTrimmedLines; } }), Object.defineProperty(t, 'diffSentences', { enumerable: !0, get: function () { return l.diffSentences; } }), Object.defineProperty(t, 'diffCss', { enumerable: !0, get: function () { return c.diffCss; } }), Object.defineProperty(t, 'diffJson', { enumerable: !0, get: function () { return d.diffJson; } }), Object.defineProperty(t, 'canonicalize', { enumerable: !0, get: function () { return d.canonicalize; } }), Object.defineProperty(t, 'diffArrays', { enumerable: !0, get: function () { return f.diffArrays; } }), Object.defineProperty(t, 'applyPatch', { enumerable: !0, get: function () { return u.applyPatch; } }), Object.defineProperty(t, 'applyPatches', { enumerable: !0, get: function () { return u.applyPatches; } }), Object.defineProperty(t, 'parsePatch', { enumerable: !0, get: function () { return h.parsePatch; } }), Object.defineProperty(t, 'merge', { enumerable: !0, get: function () { return p.merge; } }), Object.defineProperty(t, 'structuredPatch', { enumerable: !0, get: function () { return b.structuredPatch; } }), Object.defineProperty(t, 'createTwoFilesPatch', { enumerable: !0, get: function () { return b.createTwoFilesPatch; } }), Object.defineProperty(t, 'createPatch', { enumerable: !0, get: function () { return b.createPatch; } }), Object.defineProperty(t, 'convertChangesToDMP', { enumerable: !0, get: function () { return g.convertChangesToDMP; } }), Object.defineProperty(t, 'convertChangesToXML', { enumerable: !0, get: function () { return m.convertChangesToXML; } }); var i, r = (i = n(913)) && i.__esModule ? i : { default: i }, s = n(630), o = n(303), a = n(187), l = n(146), c = n(852), d = n(276), f = n(976), u = n(690), h = n(719), p = n(51), b = n(286), g = n(696), m = n(826); }, 690: (e, t, n) => { 'use strict'; Object.defineProperty(t, '__esModule', { value: !0 }), (t.applyPatch = o), (t.applyPatches = function (e, t) { 'string' == typeof e && (e = (0, r.parsePatch)(e)); var n = 0; !(function i() { var r = e[n++]; if (!r) return t.complete(); t.loadFile(r, function (e, n) { if (e) return t.complete(e); var s = o(n, r, t); t.patched(r, s, function (e) { if (e) return t.complete(e); i(); }); }); })(); }); var i, r = n(719), s = (i = n(169)) && i.__esModule ? i : { default: i }; function o(e, t) { var n = arguments.length > 2 && void 0 !== arguments[2] ? arguments[2] : {}; if (('string' == typeof t && (t = (0, r.parsePatch)(t)), Array.isArray(t))) { if (t.length > 1) throw new Error('applyPatch only works with a single input.'); t = t[0]; } var i, o, a = e.split(/\r\n|[\n\v\f\r\x85]/), l = e.match(/\r\n|[\n\v\f\r\x85]/g) || [], c = t.hunks, d = n.compareLine || function (e, t, n, i) { return t === i; }, f = 0, u = n.fuzzFactor || 0, h = 0, p = 0; function b(e, t) { for (var n = 0; n < e.lines.length; n++) { var i = e.lines[n], r = i.length > 0 ? i[0] : ' ', s = i.length > 0 ? i.substr(1) : i; if (' ' === r || '-' === r) { if (!d(t + 1, a[t], r, s) && ++f > u) return !1; t++; } } return !0; } for (var g = 0; g < c.length; g++) { for ( var m = c[g], v = a.length - m.oldLines, y = 0, w = p + m.oldStart - 1, S = (0, s.default)(w, h, v); void 0 !== y; y = S() ) if (b(m, w + y)) { m.offset = p += y; break; } if (void 0 === y) return !1; h = m.offset + m.oldStart + m.oldLines; } for (var L = 0, C = 0; C < c.length; C++) { var x = c[C], O = x.oldStart + x.offset + L - 1; L += x.newLines - x.oldLines; for (var T = 0; T < x.lines.length; T++) { var j = x.lines[T], _ = j.length > 0 ? j[0] : ' ', N = j.length > 0 ? j.substr(1) : j, P = x.linedelimiters[T]; if (' ' === _) O++; else if ('-' === _) a.splice(O, 1), l.splice(O, 1); else if ('+' === _) a.splice(O, 0, N), l.splice(O, 0, P), O++; else if ('\\' === _) { var E = x.lines[T - 1] ? x.lines[T - 1][0] : null; '+' === E ? (i = !0) : '-' === E && (o = !0); } } } if (i) for (; !a[a.length - 1]; ) a.pop(), l.pop(); else o && (a.push(''), l.push('\n')); for (var M = 0; M < a.length - 1; M++) a[M] = a[M] + l[M]; return a.join(''); } }, 286: (e, t, n) => { 'use strict'; Object.defineProperty(t, '__esModule', { value: !0 }), (t.structuredPatch = o), (t.formatPatch = a), (t.createTwoFilesPatch = l), (t.createPatch = function (e, t, n, i, r, s) { return l(e, e, t, n, i, r, s); }); var i = n(187); function r(e) { return ( (function (e) { if (Array.isArray(e)) return s(e); })(e) || (function (e) { if ('undefined' != typeof Symbol && Symbol.iterator in Object(e)) return Array.from(e); })(e) || (function (e, t) { if (e) { if ('string' == typeof e) return s(e, t); var n = Object.prototype.toString.call(e).slice(8, -1); return ( 'Object' === n && e.constructor && (n = e.constructor.name), 'Map' === n || 'Set' === n ? Array.from(e) : 'Arguments' === n || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n) ? s(e, t) : void 0 ); } })(e) || (function () { throw new TypeError( 'Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.' ); })() ); } function s(e, t) { (null == t || t > e.length) && (t = e.length); for (var n = 0, i = new Array(t); n < t; n++) i[n] = e[n]; return i; } function o(e, t, n, s, o, a, l) { l || (l = {}), void 0 === l.context && (l.context = 4); var c = (0, i.diffLines)(n, s, l); if (c) { c.push({ value: '', lines: [] }); for ( var d = [], f = 0, u = 0, h = [], p = 1, b = 1, g = function (e) { var t = c[e], i = t.lines || t.value.replace(/\n$/, '').split('\n'); if (((t.lines = i), t.added || t.removed)) { var o; if (!f) { var a = c[e - 1]; (f = p), (u = b), a && ((h = l.context > 0 ? v(a.lines.slice(-l.context)) : []), (f -= h.length), (u -= h.length)); } (o = h).push.apply( o, r( i.map(function (e) { return (t.added ? '+' : '-') + e; }) ) ), t.added ? (b += i.length) : (p += i.length); } else { if (f) if (i.length <= 2 * l.context && e < c.length - 2) { var g; (g = h).push.apply(g, r(v(i))); } else { var m, y = Math.min(i.length, l.context); (m = h).push.apply(m, r(v(i.slice(0, y)))); var w = { oldStart: f, oldLines: p - f + y, newStart: u, newLines: b - u + y, lines: h }; if (e >= c.length - 2 && i.length <= l.context) { var S = /\n$/.test(n), L = /\n$/.test(s), C = 0 == i.length && h.length > w.oldLines; !S && C && n.length > 0 && h.splice(w.oldLines, 0, '\\ No newline at end of file'), ((S || C) && L) || h.push('\\ No newline at end of file'); } d.push(w), (f = 0), (u = 0), (h = []); } (p += i.length), (b += i.length); } }, m = 0; m < c.length; m++ ) g(m); return { oldFileName: e, newFileName: t, oldHeader: o, newHeader: a, hunks: d }; } function v(e) { return e.map(function (e) { return ' ' + e; }); } } function a(e) { var t = []; e.oldFileName == e.newFileName && t.push('Index: ' + e.oldFileName), t.push('==================================================================='), t.push('--- ' + e.oldFileName + (void 0 === e.oldHeader ? '' : '\t' + e.oldHeader)), t.push('+++ ' + e.newFileName + (void 0 === e.newHeader ? '' : '\t' + e.newHeader)); for (var n = 0; n < e.hunks.length; n++) { var i = e.hunks[n]; 0 === i.oldLines && (i.oldStart -= 1), 0 === i.newLines && (i.newStart -= 1), t.push('@@ -' + i.oldStart + ',' + i.oldLines + ' +' + i.newStart + ',' + i.newLines + ' @@'), t.push.apply(t, i.lines); } return t.join('\n') + '\n'; } function l(e, t, n, i, r, s, l) { return a(o(e, t, n, i, r, s, l)); } }, 51: (e, t, n) => { 'use strict'; Object.defineProperty(t, '__esModule', { value: !0 }), (t.calcLineCount = l), (t.merge = function (e, t, n) { (e = c(e, n)), (t = c(t, n)); var i = {}; (e.index || t.index) && (i.index = e.index || t.index), (e.newFileName || t.newFileName) && (d(e) ? d(t) ? ((i.oldFileName = f(i, e.oldFileName, t.oldFileName)), (i.newFileName = f(i, e.newFileName, t.newFileName)), (i.oldHeader = f(i, e.oldHeader, t.oldHeader)), (i.newHeader = f(i, e.newHeader, t.newHeader))) : ((i.oldFileName = e.oldFileName), (i.newFileName = e.newFileName), (i.oldHeader = e.oldHeader), (i.newHeader = e.newHeader)) : ((i.oldFileName = t.oldFileName || e.oldFileName), (i.newFileName = t.newFileName || e.newFileName), (i.oldHeader = t.oldHeader || e.oldHeader), (i.newHeader = t.newHeader || e.newHeader))), (i.hunks = []); for (var r = 0, s = 0, o = 0, a = 0; r < e.hunks.length || s < t.hunks.length; ) { var l = e.hunks[r] || { oldStart: 1 / 0 }, b = t.hunks[s] || { oldStart: 1 / 0 }; if (u(l, b)) i.hunks.push(h(l, o)), r++, (a += l.newLines - l.oldLines); else if (u(b, l)) i.hunks.push(h(b, a)), s++, (o += b.newLines - b.oldLines); else { var g = { oldStart: Math.min(l.oldStart, b.oldStart), oldLines: 0, newStart: Math.min(l.newStart + o, b.oldStart + a), newLines: 0, lines: [] }; p(g, l.oldStart, l.lines, b.oldStart, b.lines), s++, r++, i.hunks.push(g); } } return i; }); var i = n(286), r = n(719), s = n(780); function o(e) { return ( (function (e) { if (Array.isArray(e)) return a(e); })(e) || (function (e) { if ('undefined' != typeof Symbol && Symbol.iterator in Object(e)) return Array.from(e); })(e) || (function (e, t) { if (e) { if ('string' == typeof e) return a(e, t); var n = Object.prototype.toString.call(e).slice(8, -1); return ( 'Object' === n && e.constructor && (n = e.constructor.name), 'Map' === n || 'Set' === n ? Array.from(e) : 'Arguments' === n || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n) ? a(e, t) : void 0 ); } })(e) || (function () { throw new TypeError( 'Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.' ); })() ); } function a(e, t) { (null == t || t > e.length) && (t = e.length); for (var n = 0, i = new Array(t); n < t; n++) i[n] = e[n]; return i; } function l(e) { var t = C(e.lines), n = t.oldLines, i = t.newLines; void 0 !== n ? (e.oldLines = n) : delete e.oldLines, void 0 !== i ? (e.newLines = i) : delete e.newLines; } function c(e, t) { if ('string' == typeof e) { if (/^@@/m.test(e) || /^Index:/m.test(e)) return (0, r.parsePatch)(e)[0]; if (!t) throw new Error('Must provide a base reference or pass in a patch'); return (0, i.structuredPatch)(void 0, void 0, t, e); } return e; } function d(e) { return e.newFileName && e.newFileName !== e.oldFileName; } function f(e, t, n) { return t === n ? t : ((e.conflict = !0), { mine: t, theirs: n }); } function u(e, t) { return e.oldStart < t.oldStart && e.oldStart + e.oldLines < t.oldStart; } function h(e, t) { return { oldStart: e.oldStart, oldLines: e.oldLines, newStart: e.newStart + t, newLines: e.newLines, lines: e.lines }; } function p(e, t, n, i, r) { var s = { offset: t, lines: n, index: 0 }, a = { offset: i, lines: r, index: 0 }; for (v(e, s, a), v(e, a, s); s.index < s.lines.length && a.index < a.lines.length; ) { var c = s.lines[s.index], d = a.lines[a.index]; if (('-' !== c[0] && '+' !== c[0]) || ('-' !== d[0] && '+' !== d[0])) if ('+' === c[0] && ' ' === d[0]) { var f; (f = e.lines).push.apply(f, o(w(s))); } else if ('+' === d[0] && ' ' === c[0]) { var u; (u = e.lines).push.apply(u, o(w(a))); } else '-' === c[0] && ' ' === d[0] ? g(e, s, a) : '-' === d[0] && ' ' === c[0] ? g(e, a, s, !0) : c === d ? (e.lines.push(c), s.index++, a.index++) : m(e, w(s), w(a)); else b(e, s, a); } y(e, s), y(e, a), l(e); } function b(e, t, n) { var i = w(t), r = w(n); if (S(i) && S(r)) { var a, l; if ((0, s.arrayStartsWith)(i, r) && L(n, i, i.length - r.length)) return void (a = e.lines).push.apply(a, o(i)); if ((0, s.arrayStartsWith)(r, i) && L(t, r, r.length - i.length)) return void (l = e.lines).push.apply(l, o(r)); } else if ((0, s.arrayEqual)(i, r)) { var c; return void (c = e.lines).push.apply(c, o(i)); } m(e, i, r); } function g(e, t, n, i) { var r, s = w(t), a = (function (e, t) { for (var n = [], i = [], r = 0, s = !1, o = !1; r < t.length && e.index < e.lines.length; ) { var a = e.lines[e.index], l = t[r]; if ('+' === l[0]) break; if (((s = s || ' ' !== a[0]), i.push(l), r++, '+' === a[0])) for (o = !0; '+' === a[0]; ) n.push(a), (a = e.lines[++e.index]); l.substr(1) === a.substr(1) ? (n.push(a), e.index++) : (o = !0); } if (('+' === (t[r] || '')[0] && s && (o = !0), o)) return n; for (; r < t.length; ) i.push(t[r++]); return { merged: i, changes: n }; })(n, s); a.merged ? (r = e.lines).push.apply(r, o(a.merged)) : m(e, i ? a : s, i ? s : a); } function m(e, t, n) { (e.conflict = !0), e.lines.push({ conflict: !0, mine: t, theirs: n }); } function v(e, t, n) { for (; t.offset < n.offset && t.index < t.lines.length; ) { var i = t.lines[t.index++]; e.lines.push(i), t.offset++; } } function y(e, t) { for (; t.index < t.lines.length; ) { var n = t.lines[t.index++]; e.lines.push(n); } } function w(e) { for (var t = [], n = e.lines[e.index][0]; e.index < e.lines.length; ) { var i = e.lines[e.index]; if (('-' === n && '+' === i[0] && (n = '+'), n !== i[0])) break; t.push(i), e.index++; } return t; } function S(e) { return e.reduce(function (e, t) { return e && '-' === t[0]; }, !0); } function L(e, t, n) { for (var i = 0; i < n; i++) { var r = t[t.length - n + i].substr(1); if (e.lines[e.index + i] !== ' ' + r) return !1; } return (e.index += n), !0; } function C(e) { var t = 0, n = 0; return ( e.forEach(function (e) { if ('string' != typeof e) { var i = C(e.mine), r = C(e.theirs); void 0 !== t && (i.oldLines === r.oldLines ? (t += i.oldLines) : (t = void 0)), void 0 !== n && (i.newLines === r.newLines ? (n += i.newLines) : (n = void 0)); } else void 0 === n || ('+' !== e[0] && ' ' !== e[0]) || n++, void 0 === t || ('-' !== e[0] && ' ' !== e[0]) || t++; }), { oldLines: t, newLines: n } ); } }, 719: (e, t) => { 'use strict'; Object.defineProperty(t, '__esModule', { value: !0 }), (t.parsePatch = function (e) { var t = arguments.length > 1 && void 0 !== arguments[1] ? arguments[1] : {}, n = e.split(/\r\n|[\n\v\f\r\x85]/), i = e.match(/\r\n|[\n\v\f\r\x85]/g) || [], r = [], s = 0; function o() { var e = {}; for (r.push(e); s < n.length; ) { var i = n[s]; if (/^(\-\-\-|\+\+\+|@@)\s/.test(i)) break; var o = /^(?:Index:|diff(?: -r \w+)+)\s+(.+?)\s*$/.exec(i); o && (e.index = o[1]), s++; } for (a(e), a(e), e.hunks = []; s < n.length; ) { var c = n[s]; if (/^(Index:|diff|\-\-\-|\+\+\+)\s/.test(c)) break; if (/^@@/.test(c)) e.hunks.push(l()); else { if (c && t.strict) throw new Error('Unknown line ' + (s + 1) + ' ' + JSON.stringify(c)); s++; } } } function a(e) { var t = /^(---|\+\+\+)\s+(.*)$/.exec(n[s]); if (t) { var i = '---' === t[1] ? 'old' : 'new', r = t[2].split('\t', 2), o = r[0].replace(/\\\\/g, '\\'); /^".*"$/.test(o) && (o = o.substr(1, o.length - 2)), (e[i + 'FileName'] = o), (e[i + 'Header'] = (r[1] || '').trim()), s++; } } function l() { var e = s, r = n[s++].split(/@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/), o = { oldStart: +r[1], oldLines: void 0 === r[2] ? 1 : +r[2], newStart: +r[3], newLines: void 0 === r[4] ? 1 : +r[4], lines: [], linedelimiters: [] }; 0 === o.oldLines && (o.oldStart += 1), 0 === o.newLines && (o.newStart += 1); for ( var a = 0, l = 0; s < n.length && !( 0 === n[s].indexOf('--- ') && s + 2 < n.length && 0 === n[s + 1].indexOf('+++ ') && 0 === n[s + 2].indexOf('@@') ); s++ ) { var c = 0 == n[s].length && s != n.length - 1 ? ' ' : n[s][0]; if ('+' !== c && '-' !== c && ' ' !== c && '\\' !== c) break; o.lines.push(n[s]), o.linedelimiters.push(i[s] || '\n'), '+' === c ? a++ : '-' === c ? l++ : ' ' === c && (a++, l++); } if ((a || 1 !== o.newLines || (o.newLines = 0), l || 1 !== o.oldLines || (o.oldLines = 0), t.strict)) { if (a !== o.newLines) throw new Error('Added line count did not match for hunk at line ' + (e + 1)); if (l !== o.oldLines) throw new Error('Removed line count did not match for hunk at line ' + (e + 1)); } return o; } for (; s < n.length; ) o(); return r; }); }, 780: (e, t) => { 'use strict'; function n(e, t) { if (t.length > e.length) return !1; for (var n = 0; n < t.length; n++) if (t[n] !== e[n]) return !1; return !0; } Object.defineProperty(t, '__esModule', { value: !0 }), (t.arrayEqual = function (e, t) { return e.length === t.length && n(e, t); }), (t.arrayStartsWith = n); }, 169: (e, t) => { 'use strict'; Object.defineProperty(t, '__esModule', { value: !0 }), (t.default = function (e, t, n) { var i = !0, r = !1, s = !1, o = 1; return function a() { if (i && !s) { if ((r ? o++ : (i = !1), e + o <= n)) return o; s = !0; } if (!r) return s || (i = !0), t <= e - o ? -o++ : ((r = !0), a()); }; }); }, 9: (e, t) => { 'use strict'; Object.defineProperty(t, '__esModule', { value: !0 }), (t.generateOptions = function (e, t) { if ('function' == typeof e) t.callback = e; else if (e) for (var n in e) e.hasOwnProperty(n) && (t[n] = e[n]); return t; }); }, 397: (e, t) => { !(function (e) { var t = /\S/, n = /\"/g, i = /\n/g, r = /\r/g, s = /\\/g, o = /\u2028/, a = /\u2029/; function l(e) { return e.trim ? e.trim() : e.replace(/^\s*|\s*$/g, ''); } function c(e, t, n) { if (t.charAt(n) != e.charAt(0)) return !1; for (var i = 1, r = e.length; i < r; i++) if (t.charAt(n + i) != e.charAt(i)) return !1; return !0; } (e.tags = { '#': 1, '^': 2, '<': 3, $: 4, '/': 5, '!': 6, '>': 7, '=': 8, _v: 9, '{': 10, '&': 11, _t: 12 }), (e.scan = function (n, i) { var r, s = n.length, o = 0, a = null, d = null, f = '', u = [], h = !1, p = 0, b = 0, g = '{{', m = '}}'; function v() { f.length > 0 && (u.push({ tag: '_t', text: new String(f) }), (f = '')); } function y(n, i) { if ( (v(), n && (function () { for (var n = !0, i = b; i < u.length; i++) if (!(n = e.tags[u[i].tag] < e.tags._v || ('_t' == u[i].tag && null === u[i].text.match(t)))) return !1; return n; })()) ) for (var r, s = b; s < u.length; s++) u[s].text && ((r = u[s + 1]) && '>' == r.tag && (r.indent = u[s].text.toString()), u.splice(s, 1)); else i || u.push({ tag: '\n' }); (h = !1), (b = u.length); } function w(e, t) { var n = '=' + m, i = e.indexOf(n, t), r = l(e.substring(e.indexOf('=', t) + 1, i)).split(' '); return (g = r[0]), (m = r[r.length - 1]), i + n.length - 1; } for (i && ((i = i.split(' ')), (g = i[0]), (m = i[1])), p = 0; p < s; p++) 0 == o ? c(g, n, p) ? (--p, v(), (o = 1)) : '\n' == n.charAt(p) ? y(h) : (f += n.charAt(p)) : 1 == o ? ((p += g.length - 1), '=' == (a = (d = e.tags[n.charAt(p + 1)]) ? n.charAt(p + 1) : '_v') ? ((p = w(n, p)), (o = 0)) : (d && p++, (o = 2)), (h = p)) : c(m, n, p) ? (u.push({ tag: a, n: l(f), otag: g, ctag: m, i: '/' == a ? h - g.length : p + m.length }), (f = ''), (p += m.length - 1), (o = 0), '{' == a && ('}}' == m ? p++ : '}' === (r = u[u.length - 1]).n.substr(r.n.length - 1) && (r.n = r.n.substring(0, r.n.length - 1)))) : (f += n.charAt(p)); return y(h, !0), u; }); var d = { _t: !0, '\n': !0, $: !0, '/': !0 }; function f(t, n, i, r) { var s, o = [], a = null, l = null; for (s = i[i.length - 1]; t.length > 0; ) { if (((l = t.shift()), s && '<' == s.tag && !(l.tag in d))) throw new Error('Illegal content in < super tag.'); if (e.tags[l.tag] <= e.tags.$ || u(l, r)) i.push(l), (l.nodes = f(t, l.tag, i, r)); else { if ('/' == l.tag) { if (0 === i.length) throw new Error('Closing tag without opener: /' + l.n); if (((a = i.pop()), l.n != a.n && !h(l.n, a.n, r))) throw new Error('Nesting error: ' + a.n + ' vs. ' + l.n); return (a.end = l.i), o; } '\n' == l.tag && (l.last = 0 == t.length || '\n' == t[0].tag); } o.push(l); } if (i.length > 0) throw new Error('missing closing tag: ' + i.pop().n); return o; } function u(e, t) { for (var n = 0, i = t.length; n < i; n++) if (t[n].o == e.n) return (e.tag = '#'), !0; } function h(e, t, n) { for (var i = 0, r = n.length; i < r; i++) if (n[i].c == e && n[i].o == t) return !0; } function p(e) { var t = []; for (var n in e.partials) t.push('"' + g(n) + '":{name:"' + g(e.partials[n].name) + '", ' + p(e.partials[n]) + '}'); return ( 'partials: {' + t.join(',') + '}, subs: ' + (function (e) { var t = []; for (var n in e) t.push('"' + g(n) + '": function(c,p,t,i) {' + e[n] + '}'); return '{ ' + t.join(',') + ' }'; })(e.subs) ); } e.stringify = function (t, n, i) { return '{code: function (c,p,i) { ' + e.wrapMain(t.code) + ' },' + p(t) + '}'; }; var b = 0; function g(e) { return e .replace(s, '\\\\') .replace(n, '\\"') .replace(i, '\\n') .replace(r, '\\r') .replace(o, '\\u2028') .replace(a, '\\u2029'); } function m(e) { return ~e.indexOf('.') ? 'd' : 'f'; } function v(e, t) { var n = '<' + (t.prefix || '') + e.n + b++; return ( (t.partials[n] = { name: e.n, partials: {} }), (t.code += 't.b(t.rp("' + g(n) + '",c,p,"' + (e.indent || '') + '"));'), n ); } function y(e, t) { t.code += 't.b(t.t(t.' + m(e.n) + '("' + g(e.n) + '",c,p,0)));'; } function w(e) { return 't.b(' + e + ');'; } (e.generate = function (t, n, i) { b = 0; var r = { code: '', subs: {}, partials: {} }; return e.walk(t, r), i.asString ? this.stringify(r, n, i) : this.makeTemplate(r, n, i); }), (e.wrapMain = function (e) { return 'var t=this;t.b(i=i||"");' + e + 'return t.fl();'; }), (e.template = e.Template), (e.makeTemplate = function (e, t, n) { var i = this.makePartials(e); return (i.code = new Function('c', 'p', 'i', this.wrapMain(e.code))), new this.template(i, t, this, n); }), (e.makePartials = function (e) { var t, n = { subs: {}, partials: e.partials, name: e.name }; for (t in n.partials) n.partials[t] = this.makePartials(n.partials[t]); for (t in e.subs) n.subs[t] = new Function('c', 'p', 't', 'i', e.subs[t]); return n; }), (e.codegen = { '#': function (t, n) { (n.code += 'if(t.s(t.' + m(t.n) + '("' + g(t.n) + '",c,p,1),c,p,0,' + t.i + ',' + t.end + ',"' + t.otag + ' ' + t.ctag + '")){t.rs(c,p,function(c,p,t){'), e.walk(t.nodes, n), (n.code += '});c.pop();}'); }, '^': function (t, n) { (n.code += 'if(!t.s(t.' + m(t.n) + '("' + g(t.n) + '",c,p,1),c,p,1,0,0,"")){'), e.walk(t.nodes, n), (n.code += '};'); }, '>': v, '<': function (t, n) { var i = { partials: {}, code: '', subs: {}, inPartial: !0 }; e.walk(t.nodes, i); var r = n.partials[v(t, n)]; (r.subs = i.subs), (r.partials = i.partials); }, $: function (t, n) { var i = { subs: {}, code: '', partials: n.partials, prefix: t.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': function (e, t) { t.code += w('"\\n"' + (e.last ? '' : ' + i')); }, _v: function (e, t) { t.code += 't.b(t.v(t.' + m(e.n) + '("' + g(e.n) + '",c,p,0)));'; }, _t: function (e, t) { t.code += w('"' + g(e.text) + '"'); }, '{': y, '&': y }), (e.walk = function (t, n) { for (var i, r = 0, s = t.length; r < s; r++) (i = e.codegen[t[r].tag]) && i(t[r], n); return n; }), (e.parse = function (e, t, n) { return f(e, 0, [], (n = n || {}).sectionTags || []); }), (e.cache = {}), (e.cacheKey = function (e, t) { return [e, !!t.asString, !!t.disableLambda, t.delimiters, !!t.modelGet].join('||'); }), (e.compile = function (t, n) { n = n || {}; var i = e.cacheKey(t, n), r = this.cache[i]; if (r) { var s = r.partials; for (var o in s) delete s[o].instance; return r; } return (r = this.generate(this.parse(this.scan(t, n.delimiters), t, n), t, n)), (this.cache[i] = r); }); })(t); }, 485: (e, t, n) => { var i = n(397); (i.Template = n(882).Template), (i.template = i.Template), (e.exports = i); }, 882: (e, t) => { !(function (e) { function t(e, t, n) { var i; return ( t && 'object' == typeof t && (void 0 !== t[e] ? (i = t[e]) : n && t.get && 'function' == typeof t.get && (i = t.get(e))), i ); } (e.Template = function (e, t, n, i) { (e = e || {}), (this.r = e.code || this.r), (this.c = n), (this.options = i || {}), (this.text = t || ''), (this.partials = e.partials || {}), (this.subs = e.subs || {}), (this.buf = ''); }), (e.Template.prototype = { r: function (e, t, n) { return ''; }, v: function (e) { return ( (e = l(e)), a.test(e) ? e .replace(n, '&') .replace(i, '<') .replace(r, '>') .replace(s, ''') .replace(o, '"') : e ); }, t: l, render: function (e, t, n) { return this.ri([e], t || {}, n); }, ri: function (e, t, n) { return this.r(e, t, n); }, ep: function (e, t) { var n = this.partials[e], i = t[n.name]; if (n.instance && n.base == i) return n.instance; if ('string' == typeof i) { if (!this.c) throw new Error('No compiler available.'); i = this.c.compile(i, this.options); } if (!i) return null; if (((this.partials[e].base = i), n.subs)) { for (key in (t.stackText || (t.stackText = {}), n.subs)) t.stackText[key] || (t.stackText[key] = void 0 !== this.activeSub && t.stackText[this.activeSub] ? t.stackText[this.activeSub] : this.text); i = (function (e, t, n, i, r, s) { function o() {} function a() {} var l; (o.prototype = e), (a.prototype = e.subs); var c = new o(); for (l in ((c.subs = new a()), (c.subsText = {}), (c.buf = ''), (i = i || {}), (c.stackSubs = i), (c.subsText = s), t)) i[l] || (i[l] = t[l]); for (l in i) c.subs[l] = i[l]; for (l in ((r = r || {}), (c.stackPartials = r), n)) r[l] || (r[l] = n[l]); for (l in r) c.partials[l] = r[l]; return c; })(i, n.subs, n.partials, this.stackSubs, this.stackPartials, t.stackText); } return (this.partials[e].instance = i), i; }, rp: function (e, t, n, i) { var r = this.ep(e, n); return r ? r.ri(t, n, i) : ''; }, rs: function (e, t, n) { var i = e[e.length - 1]; if (c(i)) for (var r = 0; r < i.length; r++) e.push(i[r]), n(e, t, this), e.pop(); else n(e, t, this); }, s: function (e, t, n, i, r, s, o) { var a; return ( (!c(e) || 0 !== e.length) && ('function' == typeof e && (e = this.ms(e, t, n, i, r, s, o)), (a = !!e), !i && a && t && t.push('object' == typeof e ? e : t[t.length - 1]), a) ); }, d: function (e, n, i, r) { var s, o = e.split('.'), a = this.f(o[0], n, i, r), l = this.options.modelGet, d = null; if ('.' === e && c(n[n.length - 2])) a = n[n.length - 1]; else for (var f = 1; f < o.length; f++) void 0 !== (s = t(o[f], a, l)) ? ((d = a), (a = s)) : (a = ''); return !(r && !a) && (r || 'function' != typeof a || (n.push(d), (a = this.mv(a, n, i)), n.pop()), a); }, f: function (e, n, i, r) { for (var s = !1, o = !1, a = this.options.modelGet, l = n.length - 1; l >= 0; l--) if (void 0 !== (s = t(e, n[l], a))) { o = !0; break; } return o ? (r || 'function' != typeof s || (s = this.mv(s, n, i)), s) : !r && ''; }, ls: function (e, t, n, i, r) { var s = this.options.delimiters; return ( (this.options.delimiters = r), this.b(this.ct(l(e.call(t, i)), t, n)), (this.options.delimiters = s), !1 ); }, ct: function (e, t, n) { if (this.options.disableLambda) throw new Error('Lambda features disabled.'); return this.c.compile(e, this.options).render(t, n); }, b: function (e) { this.buf += e; }, fl: function () { var e = this.buf; return (this.buf = ''), e; }, ms: function (e, t, n, i, r, s, o) { var a, l = t[t.length - 1], c = e.call(l); return 'function' == typeof c ? !!i || ((a = this.activeSub && this.subsText && this.subsText[this.activeSub] ? this.subsText[this.activeSub] : this.text), this.ls(c, l, n, a.substring(r, s), o)) : c; }, mv: function (e, t, n) { var i = t[t.length - 1], r = e.call(i); return 'function' == typeof r ? this.ct(l(r.call(i)), i, n) : r; }, sub: function (e, t, n, i) { var r = this.subs[e]; r && ((this.activeSub = e), r(t, n, this, i), (this.activeSub = !1)); } }); var n = /&/g, i = //g, s = /\'/g, o = /\"/g, a = /[&<>\"\']/; function l(e) { return String(null == e ? '' : e); } var c = Array.isArray || function (e) { return '[object Array]' === Object.prototype.toString.call(e); }; })(t); }, 468: (e, t, n) => { 'use strict'; Object.defineProperty(t, '__esModule', { value: !0 }), (t.parse = void 0); const i = n(699), r = n(593); function s(e, t) { const n = e.split('.'); return n.length > 1 ? n[n.length - 1] : t; } function o(e, t) { return t.reduce((t, n) => t || e.startsWith(n), !1); } const a = ['a/', 'b/', 'i/', 'w/', 'c/', 'o/']; function l(e, t, n) { const i = void 0 !== n ? [...a, n] : a, s = t ? new RegExp(`^${(0, r.escapeForRegExp)(t)} "?(.+?)"?$`) : new RegExp('^"?(.+?)"?$'), [, o = ''] = s.exec(e) || [], l = i.find((e) => 0 === o.indexOf(e)); return (l ? o.slice(l.length) : o).replace( /\s+\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}(?:\.\d+)? [+-]\d{4}.*$/, '' ); } t.parse = function (e, t = {}) { const n = []; let r = null, a = null, c = null, d = null, f = null, u = null, h = null; const p = '--- ', b = '+++ ', g = '@@', m = /^old mode (\d{6})/, v = /^new mode (\d{6})/, y = /^deleted file mode (\d{6})/, w = /^new file mode (\d{6})/, S = /^copy from "?(.+)"?/, L = /^copy to "?(.+)"?/, C = /^rename from "?(.+)"?/, x = /^rename to "?(.+)"?/, O = /^similarity index (\d+)%/, T = /^dissimilarity index (\d+)%/, j = /^index ([\da-z]+)\.\.([\da-z]+)\s*(\d{6})?/, _ = /^Binary files (.*) and (.*) differ/, N = /^GIT binary patch/, P = /^index ([\da-z]+),([\da-z]+)\.\.([\da-z]+)/, E = /^mode (\d{6}),(\d{6})\.\.(\d{6})/, M = /^new file mode (\d{6})/, H = /^deleted file mode (\d{6}),(\d{6})/, k = e .replace(/\\ No newline at end of file/g, '') .replace(/\r\n?/g, '\n') .split('\n'); function D() { null !== a && null !== r && (r.blocks.push(a), (a = null)); } function F() { null !== r && (r.oldName || null === u || (r.oldName = u), r.newName || null === h || (r.newName = h), r.newName && (n.push(r), (r = null))), (u = null), (h = null); } function I() { D(), F(), (r = { blocks: [], deletedLines: 0, addedLines: 0 }); } function A(e) { let t; D(), null !== r && ((t = /^@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@.*/.exec(e)) ? ((r.isCombined = !1), (c = parseInt(t[1], 10)), (f = parseInt(t[2], 10))) : (t = /^@@@ -(\d+)(?:,\d+)? -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@@.*/.exec(e)) ? ((r.isCombined = !0), (c = parseInt(t[1], 10)), (d = parseInt(t[2], 10)), (f = parseInt(t[3], 10))) : (e.startsWith(g) && console.error('Failed to parse lines, starting in 0!'), (c = 0), (f = 0), (r.isCombined = !1))), (a = { lines: [], oldStartLine: c, oldStartLine2: d, newStartLine: f, header: e }); } return ( k.forEach((e, d) => { if (!e || e.startsWith('*')) return; let D; const F = k[d - 1], R = k[d + 1], W = k[d + 2]; if (e.startsWith('diff --git') || e.startsWith('diff --combined')) { if ( (I(), (D = /^diff --git "?([a-ciow]\/.+)"? "?([a-ciow]\/.+)"?/.exec(e)) && ((u = l(D[1], void 0, t.dstPrefix)), (h = l(D[2], void 0, t.srcPrefix))), null === r) ) throw new Error('Where is my file !!!'); return void (r.isGitDiff = !0); } if (e.startsWith('Binary files') && !(null == r ? void 0 : r.isGitDiff)) { if ( (I(), (D = /^Binary files "?([a-ciow]\/.+)"? and "?([a-ciow]\/.+)"? differ/.exec(e)) && ((u = l(D[1], void 0, t.dstPrefix)), (h = l(D[2], void 0, t.srcPrefix))), null === r) ) throw new Error('Where is my file !!!'); return void (r.isBinary = !0); } if ( ((!r || (!r.isGitDiff && r && e.startsWith(p) && R.startsWith(b) && W.startsWith(g))) && I(), null == r ? void 0 : r.isTooBig) ) return; if ( r && (('number' == typeof t.diffMaxChanges && r.addedLines + r.deletedLines > t.diffMaxChanges) || ('number' == typeof t.diffMaxLineLength && e.length > t.diffMaxLineLength)) ) return ( (r.isTooBig = !0), (r.addedLines = 0), (r.deletedLines = 0), (r.blocks = []), (a = null), void A( 'function' == typeof t.diffTooBigMessage ? t.diffTooBigMessage(n.length) : 'Diff too big to be displayed' ) ); if ((e.startsWith(p) && R.startsWith(b)) || (e.startsWith(b) && F.startsWith(p))) { if ( r && !r.oldName && e.startsWith('--- ') && (D = (function (e, t) { return l(e, '---', t); })(e, t.srcPrefix)) ) return (r.oldName = D), void (r.language = s(r.oldName, r.language)); if ( r && !r.newName && e.startsWith('+++ ') && (D = (function (e, t) { return l(e, '+++', t); })(e, t.dstPrefix)) ) return (r.newName = D), void (r.language = s(r.newName, r.language)); } if (r && (e.startsWith(g) || (r.isGitDiff && r.oldName && r.newName && !a))) return void A(e); if (a && (e.startsWith('+') || e.startsWith('-') || e.startsWith(' '))) return void (function (e) { if (null === r || null === a || null === c || null === f) return; const t = { content: e }, n = r.isCombined ? ['+ ', ' +', '++'] : ['+'], s = r.isCombined ? ['- ', ' -', '--'] : ['-']; o(e, n) ? (r.addedLines++, (t.type = i.LineType.INSERT), (t.oldNumber = void 0), (t.newNumber = f++)) : o(e, s) ? (r.deletedLines++, (t.type = i.LineType.DELETE), (t.oldNumber = c++), (t.newNumber = void 0)) : ((t.type = i.LineType.CONTEXT), (t.oldNumber = c++), (t.newNumber = f++)), a.lines.push(t); })(e); const B = !(function (e, t) { let n = t; for (; n < k.length - 3; ) { if (e.startsWith('diff')) return !1; if (k[n].startsWith(p) && k[n + 1].startsWith(b) && k[n + 2].startsWith(g)) return !0; n++; } return !1; })(e, d); if (null === r) throw new Error('Where is my file !!!'); (D = m.exec(e)) ? (r.oldMode = D[1]) : (D = v.exec(e)) ? (r.newMode = D[1]) : (D = y.exec(e)) ? ((r.deletedFileMode = D[1]), (r.isDeleted = !0)) : (D = w.exec(e)) ? ((r.newFileMode = D[1]), (r.isNew = !0)) : (D = S.exec(e)) ? (B && (r.oldName = D[1]), (r.isCopy = !0)) : (D = L.exec(e)) ? (B && (r.newName = D[1]), (r.isCopy = !0)) : (D = C.exec(e)) ? (B && (r.oldName = D[1]), (r.isRename = !0)) : (D = x.exec(e)) ? (B && (r.newName = D[1]), (r.isRename = !0)) : (D = _.exec(e)) ? ((r.isBinary = !0), (r.oldName = l(D[1], void 0, t.srcPrefix)), (r.newName = l(D[2], void 0, t.dstPrefix)), A('Binary file')) : N.test(e) ? ((r.isBinary = !0), A(e)) : (D = O.exec(e)) ? (r.unchangedPercentage = parseInt(D[1], 10)) : (D = T.exec(e)) ? (r.changedPercentage = parseInt(D[1], 10)) : (D = j.exec(e)) ? ((r.checksumBefore = D[1]), (r.checksumAfter = D[2]), D[3] && (r.mode = D[3])) : (D = P.exec(e)) ? ((r.checksumBefore = [D[2], D[3]]), (r.checksumAfter = D[1])) : (D = E.exec(e)) ? ((r.oldMode = [D[2], D[3]]), (r.newMode = D[1])) : (D = M.exec(e)) ? ((r.newFileMode = D[1]), (r.isNew = !0)) : (D = H.exec(e)) && ((r.deletedFileMode = D[1]), (r.isDeleted = !0)); }), D(), F(), n ); }; }, 979: function (e, t, n) { 'use strict'; var i = (this && this.__createBinding) || (Object.create ? function (e, t, n, i) { void 0 === i && (i = n); var r = Object.getOwnPropertyDescriptor(t, n); (r && !('get' in r ? !t.__esModule : r.writable || r.configurable)) || (r = { enumerable: !0, get: function () { return t[n]; } }), Object.defineProperty(e, i, r); } : function (e, t, n, i) { void 0 === i && (i = n), (e[i] = t[n]); }), r = (this && this.__setModuleDefault) || (Object.create ? function (e, t) { Object.defineProperty(e, 'default', { enumerable: !0, value: t }); } : function (e, t) { e.default = t; }), s = (this && this.__importStar) || function (e) { if (e && e.__esModule) return e; var t = {}; if (null != e) for (var n in e) 'default' !== n && Object.prototype.hasOwnProperty.call(e, n) && i(t, e, n); return r(t, e), t; }; Object.defineProperty(t, '__esModule', { value: !0 }), (t.defaultTemplates = void 0); const o = s(n(485)); (t.defaultTemplates = {}), (t.defaultTemplates['file-summary-line'] = new o.Template({ code: function (e, t, n) { var i = this; return ( i.b((n = n || '')), i.b('
  • '), i.b('\n' + n), i.b(' '), i.b('\n' + n), i.b(i.rp(''), i.b(i.v(i.f('fileName', e, t, 0))), i.b(''), i.b('\n' + n), i.b(' '), i.b('\n' + n), i.b(' '), i.b(i.v(i.f('addedLines', e, t, 0))), i.b(''), i.b('\n' + n), i.b(' '), i.b(i.v(i.f('deletedLines', e, t, 0))), i.b(''), i.b('\n' + n), i.b(' '), i.b('\n' + n), i.b(' '), i.b('\n' + n), i.b('
  • '), i.fl() ); }, partials: { ''), i.b('\n' + n), i.b('
    '), i.b('\n' + n), i.b(' Files changed ('), i.b(i.v(i.f('filesNumber', e, t, 0))), i.b(')'), i.b('\n' + n), i.b(' hide'), i.b('\n' + n), i.b(' show'), i.b('\n' + n), i.b('
    '), i.b('\n' + n), i.b('
      '), i.b('\n' + n), i.b(' '), i.b(i.t(i.f('files', e, t, 0))), i.b('\n' + n), i.b('
    '), i.b('\n' + n), i.b(''), i.fl() ); }, partials: {}, subs: {} })), (t.defaultTemplates['generic-block-header'] = new o.Template({ code: function (e, t, n) { var i = this; return ( i.b((n = n || '')), i.b(''), i.b('\n' + n), i.b(' '), i.b('\n' + n), i.b(' '), i.b('\n' + n), i.b('
    '), i.s(i.f('blockHeader', e, t, 1), e, t, 0, 156, 173, '{{ }}') && (i.rs(e, t, function (e, t, n) { n.b(n.t(n.f('blockHeader', e, t, 0))); }), e.pop()), i.s(i.f('blockHeader', e, t, 1), e, t, 1, 0, 0, '') || i.b(' '), i.b('
    '), i.b('\n' + n), i.b(' '), i.b('\n' + n), i.b(''), i.fl() ); }, partials: {}, subs: {} })), (t.defaultTemplates['generic-empty-diff'] = new o.Template({ code: function (e, t, n) { var i = this; return ( i.b((n = n || '')), i.b(''), i.b('\n' + n), i.b(' '), i.b('\n' + n), i.b('
    '), i.b('\n' + n), i.b(' File without changes'), i.b('\n' + n), i.b('
    '), i.b('\n' + n), i.b(' '), i.b('\n' + n), i.b(''), i.fl() ); }, partials: {}, subs: {} })), (t.defaultTemplates['generic-file-path'] = new o.Template({ code: function (e, t, n) { var i = this; return ( i.b((n = n || '')), i.b(''), i.b('\n' + n), i.b(i.rp(''), i.b(i.v(i.f('fileDiffName', e, t, 0))), i.b(''), i.b('\n' + n), i.b(i.rp(''), i.b('\n' + n), i.b(''), i.fl() ); }, partials: { ''), i.b('\n' + n), i.b(' '), i.b('\n' + n), i.b(' '), i.b(i.t(i.f('lineNumber', e, t, 0))), i.b('\n' + n), i.b(' '), i.b('\n' + n), i.b(' '), i.b('\n' + n), i.b('
    '), i.b('\n' + n), i.s(i.f('prefix', e, t, 1), e, t, 0, 162, 238, '{{ }}') && (i.rs(e, t, function (e, t, i) { i.b(' '), i.b(i.t(i.f('prefix', e, t, 0))), i.b(''), i.b('\n' + n); }), e.pop()), i.s(i.f('prefix', e, t, 1), e, t, 1, 0, 0, '') || (i.b('  '), i.b('\n' + n)), i.s(i.f('content', e, t, 1), e, t, 0, 371, 445, '{{ }}') && (i.rs(e, t, function (e, t, i) { i.b(' '), i.b(i.t(i.f('content', e, t, 0))), i.b(''), i.b('\n' + n); }), e.pop()), i.s(i.f('content', e, t, 1), e, t, 1, 0, 0, '') || (i.b('
    '), i.b('\n' + n)), i.b('
    '), i.b('\n' + n), i.b(' '), i.b('\n' + n), i.b(''), i.fl() ); }, partials: {}, subs: {} })), (t.defaultTemplates['generic-wrapper'] = new o.Template({ code: function (e, t, n) { var i = this; return ( i.b((n = n || '')), i.b('
    '), i.b('\n' + n), i.b(' '), i.b(i.t(i.f('content', e, t, 0))), i.b('\n' + n), i.b('
    '), i.fl() ); }, partials: {}, subs: {} })), (t.defaultTemplates['icon-file-added'] = new o.Template({ code: function (e, t, n) { var i = this; return ( i.b((n = n || '')), i.b( ''), i.fl() ); }, partials: {}, subs: {} })), (t.defaultTemplates['icon-file-changed'] = new o.Template({ code: function (e, t, n) { var i = this; return ( i.b((n = n || '')), i.b(''), i.fl() ); }, partials: {}, subs: {} })), (t.defaultTemplates['icon-file-deleted'] = new o.Template({ code: function (e, t, n) { var i = this; return ( i.b((n = n || '')), i.b(''), i.fl() ); }, partials: {}, subs: {} })), (t.defaultTemplates['icon-file-renamed'] = new o.Template({ code: function (e, t, n) { var i = this; return ( i.b((n = n || '')), i.b(''), i.fl() ); }, partials: {}, subs: {} })), (t.defaultTemplates['icon-file'] = new o.Template({ code: function (e, t, n) { var i = this; return ( i.b((n = n || '')), i.b( ''), i.fl() ); }, partials: {}, subs: {} })), (t.defaultTemplates['line-by-line-file-diff'] = new o.Template({ code: function (e, t, n) { var i = this; return ( i.b((n = n || '')), i.b('
    '), i.b('\n' + n), i.b('
    '), i.b('\n' + n), i.b(' '), i.b(i.t(i.f('filePath', e, t, 0))), i.b('\n' + n), i.b('
    '), i.b('\n' + n), i.b('
    '), i.b('\n' + n), i.b('
    '), i.b('\n' + n), i.b(' '), i.b('\n' + n), i.b(' '), i.b('\n' + n), i.b(' '), i.b(i.t(i.f('diffs', e, t, 0))), i.b('\n' + n), i.b(' '), i.b('\n' + n), i.b('
    '), i.b('\n' + n), i.b('
    '), i.b('\n' + n), i.b('
    '), i.b('\n' + n), i.b('
    '), i.fl() ); }, partials: {}, subs: {} })), (t.defaultTemplates['line-by-line-numbers'] = new o.Template({ code: function (e, t, n) { var i = this; return ( i.b((n = n || '')), i.b('
    '), i.b(i.v(i.f('oldNumber', e, t, 0))), i.b('
    '), i.b('\n' + n), i.b('
    '), i.b(i.v(i.f('newNumber', e, t, 0))), i.b('
    '), i.fl() ); }, partials: {}, subs: {} })), (t.defaultTemplates['side-by-side-file-diff'] = new o.Template({ code: function (e, t, n) { var i = this; return ( i.b((n = n || '')), i.b('
    '), i.b('\n' + n), i.b('
    '), i.b('\n' + n), i.b(' '), i.b(i.t(i.f('filePath', e, t, 0))), i.b('\n' + n), i.b('
    '), i.b('\n' + n), i.b('
    '), i.b('\n' + n), i.b('
    '), i.b('\n' + n), i.b('
    '), i.b('\n' + n), i.b(' '), i.b('\n' + n), i.b(' '), i.b('\n' + n), i.b(' '), i.b(i.t(i.d('diffs.left', e, t, 0))), i.b('\n' + n), i.b(' '), i.b('\n' + n), i.b('
    '), i.b('\n' + n), i.b('
    '), i.b('\n' + n), i.b('
    '), i.b('\n' + n), i.b('
    '), i.b('\n' + n), i.b('
    '), i.b('\n' + n), i.b(' '), i.b('\n' + n), i.b(' '), i.b('\n' + n), i.b(' '), i.b(i.t(i.d('diffs.right', e, t, 0))), i.b('\n' + n), i.b(' '), i.b('\n' + n), i.b('
    '), i.b('\n' + n), i.b('
    '), i.b('\n' + n), i.b('
    '), i.b('\n' + n), i.b('
    '), i.b('\n' + n), i.b('
    '), i.fl() ); }, partials: {}, subs: {} })), (t.defaultTemplates['tag-file-added'] = new o.Template({ code: function (e, t, n) { var i = this; return i.b((n = n || '')), i.b('ADDED'), i.fl(); }, partials: {}, subs: {} })), (t.defaultTemplates['tag-file-changed'] = new o.Template({ code: function (e, t, n) { var i = this; return ( i.b((n = n || '')), i.b('CHANGED'), i.fl() ); }, partials: {}, subs: {} })), (t.defaultTemplates['tag-file-deleted'] = new o.Template({ code: function (e, t, n) { var i = this; return ( i.b((n = n || '')), i.b('DELETED'), i.fl() ); }, partials: {}, subs: {} })), (t.defaultTemplates['tag-file-renamed'] = new o.Template({ code: function (e, t, n) { var i = this; return i.b((n = n || '')), i.b('RENAMED'), i.fl(); }, partials: {}, subs: {} })); }, 834: function (e, t, n) { 'use strict'; var i = (this && this.__createBinding) || (Object.create ? function (e, t, n, i) { void 0 === i && (i = n); var r = Object.getOwnPropertyDescriptor(t, n); (r && !('get' in r ? !t.__esModule : r.writable || r.configurable)) || (r = { enumerable: !0, get: function () { return t[n]; } }), Object.defineProperty(e, i, r); } : function (e, t, n, i) { void 0 === i && (i = n), (e[i] = t[n]); }), r = (this && this.__setModuleDefault) || (Object.create ? function (e, t) { Object.defineProperty(e, 'default', { enumerable: !0, value: t }); } : function (e, t) { e.default = t; }), s = (this && this.__importStar) || function (e) { if (e && e.__esModule) return e; var t = {}; if (null != e) for (var n in e) 'default' !== n && Object.prototype.hasOwnProperty.call(e, n) && i(t, e, n); return r(t, e), t; }, o = (this && this.__importDefault) || function (e) { return e && e.__esModule ? e : { default: e }; }; Object.defineProperty(t, '__esModule', { value: !0 }), (t.html = t.parse = t.defaultDiff2HtmlConfig = void 0); const a = s(n(468)), l = n(479), c = s(n(378)), d = s(n(170)), f = n(699), u = o(n(63)); (t.defaultDiff2HtmlConfig = Object.assign( Object.assign(Object.assign({}, c.defaultLineByLineRendererConfig), d.defaultSideBySideRendererConfig), { outputFormat: f.OutputFormatType.LINE_BY_LINE, drawFileList: !0 } )), (t.parse = function (e, n = {}) { return a.parse(e, Object.assign(Object.assign({}, t.defaultDiff2HtmlConfig), n)); }), (t.html = function (e, n = {}) { const i = Object.assign(Object.assign({}, t.defaultDiff2HtmlConfig), n), r = 'string' == typeof e ? a.parse(e, i) : e, s = new u.default(i), { colorScheme: o } = i, f = { colorScheme: o }; return ( (i.drawFileList ? new l.FileListRenderer(s, f).render(r) : '') + ('side-by-side' === i.outputFormat ? new d.default(s, i).render(r) : new c.default(s, i).render(r)) ); }); }, 479: function (e, t, n) { 'use strict'; var i = (this && this.__createBinding) || (Object.create ? function (e, t, n, i) { void 0 === i && (i = n); var r = Object.getOwnPropertyDescriptor(t, n); (r && !('get' in r ? !t.__esModule : r.writable || r.configurable)) || (r = { enumerable: !0, get: function () { return t[n]; } }), Object.defineProperty(e, i, r); } : function (e, t, n, i) { void 0 === i && (i = n), (e[i] = t[n]); }), r = (this && this.__setModuleDefault) || (Object.create ? function (e, t) { Object.defineProperty(e, 'default', { enumerable: !0, value: t }); } : function (e, t) { e.default = t; }), s = (this && this.__importStar) || function (e) { if (e && e.__esModule) return e; var t = {}; if (null != e) for (var n in e) 'default' !== n && Object.prototype.hasOwnProperty.call(e, n) && i(t, e, n); return r(t, e), t; }; Object.defineProperty(t, '__esModule', { value: !0 }), (t.FileListRenderer = t.defaultFileListRendererConfig = void 0); const o = s(n(741)), a = 'file-summary'; (t.defaultFileListRendererConfig = { colorScheme: o.defaultRenderConfig.colorScheme }), (t.FileListRenderer = class { constructor(e, n = {}) { (this.hoganUtils = e), (this.config = Object.assign(Object.assign({}, t.defaultFileListRendererConfig), n)); } render(e) { const t = e .map((e) => this.hoganUtils.render( a, 'line', { fileHtmlId: o.getHtmlId(e), oldName: e.oldName, newName: e.newName, fileName: o.filenameDiff(e), deletedLines: '-' + e.deletedLines, addedLines: '+' + e.addedLines }, { fileIcon: this.hoganUtils.template('icon', o.getFileIcon(e)) } ) ) .join('\n'); return this.hoganUtils.render(a, 'wrapper', { colorScheme: o.colorSchemeToCss(this.config.colorScheme), filesNumber: e.length, files: t }); } }); }, 63: function (e, t, n) { 'use strict'; var i = (this && this.__createBinding) || (Object.create ? function (e, t, n, i) { void 0 === i && (i = n); var r = Object.getOwnPropertyDescriptor(t, n); (r && !('get' in r ? !t.__esModule : r.writable || r.configurable)) || (r = { enumerable: !0, get: function () { return t[n]; } }), Object.defineProperty(e, i, r); } : function (e, t, n, i) { void 0 === i && (i = n), (e[i] = t[n]); }), r = (this && this.__setModuleDefault) || (Object.create ? function (e, t) { Object.defineProperty(e, 'default', { enumerable: !0, value: t }); } : function (e, t) { e.default = t; }), s = (this && this.__importStar) || function (e) { if (e && e.__esModule) return e; var t = {}; if (null != e) for (var n in e) 'default' !== n && Object.prototype.hasOwnProperty.call(e, n) && i(t, e, n); return r(t, e), t; }; Object.defineProperty(t, '__esModule', { value: !0 }); const o = s(n(485)), a = n(979); t.default = class { constructor({ compiledTemplates: e = {}, rawTemplates: t = {} }) { const n = Object.entries(t).reduce((e, [t, n]) => { const i = o.compile(n, { asString: !1 }); return Object.assign(Object.assign({}, e), { [t]: i }); }, {}); this.preCompiledTemplates = Object.assign(Object.assign(Object.assign({}, a.defaultTemplates), e), n); } static compile(e) { return o.compile(e, { asString: !1 }); } render(e, t, n, i, r) { const s = this.templateKey(e, t); try { return this.preCompiledTemplates[s].render(n, i, r); } catch (e) { throw new Error(`Could not find template to render '${s}'`); } } template(e, t) { return this.preCompiledTemplates[this.templateKey(e, t)]; } templateKey(e, t) { return `${e}-${t}`; } }; }, 378: function (e, t, n) { 'use strict'; var i = (this && this.__createBinding) || (Object.create ? function (e, t, n, i) { void 0 === i && (i = n); var r = Object.getOwnPropertyDescriptor(t, n); (r && !('get' in r ? !t.__esModule : r.writable || r.configurable)) || (r = { enumerable: !0, get: function () { return t[n]; } }), Object.defineProperty(e, i, r); } : function (e, t, n, i) { void 0 === i && (i = n), (e[i] = t[n]); }), r = (this && this.__setModuleDefault) || (Object.create ? function (e, t) { Object.defineProperty(e, 'default', { enumerable: !0, value: t }); } : function (e, t) { e.default = t; }), s = (this && this.__importStar) || function (e) { if (e && e.__esModule) return e; var t = {}; if (null != e) for (var n in e) 'default' !== n && Object.prototype.hasOwnProperty.call(e, n) && i(t, e, n); return r(t, e), t; }; Object.defineProperty(t, '__esModule', { value: !0 }), (t.defaultLineByLineRendererConfig = void 0); const o = s(n(483)), a = s(n(741)), l = n(699); t.defaultLineByLineRendererConfig = Object.assign(Object.assign({}, a.defaultRenderConfig), { renderNothingWhenEmpty: !1, matchingMaxComparisons: 2500, maxLineSizeInBlockForComparison: 200 }); const c = 'generic', d = 'line-by-line'; t.default = class { constructor(e, n = {}) { (this.hoganUtils = e), (this.config = Object.assign(Object.assign({}, t.defaultLineByLineRendererConfig), n)); } render(e) { const t = e .map((e) => { let t; return ( (t = e.blocks.length ? this.generateFileHtml(e) : this.generateEmptyDiff()), this.makeFileDiffHtml(e, t) ); }) .join('\n'); return this.hoganUtils.render(c, 'wrapper', { colorScheme: a.colorSchemeToCss(this.config.colorScheme), content: t }); } makeFileDiffHtml(e, t) { if (this.config.renderNothingWhenEmpty && Array.isArray(e.blocks) && 0 === e.blocks.length) return ''; const n = this.hoganUtils.template(d, 'file-diff'), i = this.hoganUtils.template(c, 'file-path'), r = this.hoganUtils.template('icon', 'file'), s = this.hoganUtils.template('tag', a.getFileIcon(e)); return n.render({ file: e, fileHtmlId: a.getHtmlId(e), diffs: t, filePath: i.render({ fileDiffName: a.filenameDiff(e) }, { fileIcon: r, fileTag: s }) }); } generateEmptyDiff() { return this.hoganUtils.render(c, 'empty-diff', { contentClass: 'd2h-code-line', CSSLineClass: a.CSSLineClass }); } generateFileHtml(e) { const t = o.newMatcherFn(o.newDistanceFn((t) => a.deconstructLine(t.content, e.isCombined).content)); return e.blocks .map((n) => { let i = this.hoganUtils.render(c, 'block-header', { CSSLineClass: a.CSSLineClass, blockHeader: e.isTooBig ? n.header : a.escapeForHtml(n.header), lineClass: 'd2h-code-linenumber', contentClass: 'd2h-code-line' }); return ( this.applyLineGroupping(n).forEach(([n, r, s]) => { if (r.length && s.length && !n.length) this.applyRematchMatching(r, s, t).map(([t, n]) => { const { left: r, right: s } = this.processChangedLines(e, e.isCombined, t, n); (i += r), (i += s); }); else if (n.length) n.forEach((t) => { const { prefix: n, content: r } = a.deconstructLine(t.content, e.isCombined); i += this.generateSingleLineHtml(e, { type: a.CSSLineClass.CONTEXT, prefix: n, content: r, oldNumber: t.oldNumber, newNumber: t.newNumber }); }); else if (r.length || s.length) { const { left: t, right: n } = this.processChangedLines(e, e.isCombined, r, s); (i += t), (i += n); } else console.error('Unknown state reached while processing groups of lines', n, r, s); }), i ); }) .join('\n'); } applyLineGroupping(e) { const t = []; let n = [], i = []; for (let r = 0; r < e.lines.length; r++) { const s = e.lines[r]; ((s.type !== l.LineType.INSERT && i.length) || (s.type === l.LineType.CONTEXT && n.length > 0)) && (t.push([[], n, i]), (n = []), (i = [])), s.type === l.LineType.CONTEXT ? t.push([[s], [], []]) : s.type === l.LineType.INSERT && 0 === n.length ? t.push([[], [], [s]]) : s.type === l.LineType.INSERT && n.length > 0 ? i.push(s) : s.type === l.LineType.DELETE && n.push(s); } return (n.length || i.length) && (t.push([[], n, i]), (n = []), (i = [])), t; } applyRematchMatching(e, t, n) { const i = e.length * t.length, r = Math.max.apply(null, [0].concat(e.concat(t).map((e) => e.content.length))); return i < this.config.matchingMaxComparisons && r < this.config.maxLineSizeInBlockForComparison && ('lines' === this.config.matching || 'words' === this.config.matching) ? n(e, t) : [[e, t]]; } processChangedLines(e, t, n, i) { const r = { right: '', left: '' }, s = Math.max(n.length, i.length); for (let o = 0; o < s; o++) { const s = n[o], l = i[o], c = void 0 !== s && void 0 !== l ? a.diffHighlight(s.content, l.content, t, this.config) : void 0, d = void 0 !== s && void 0 !== s.oldNumber ? Object.assign( Object.assign( {}, void 0 !== c ? { prefix: c.oldLine.prefix, content: c.oldLine.content, type: a.CSSLineClass.DELETE_CHANGES } : Object.assign(Object.assign({}, a.deconstructLine(s.content, t)), { type: a.toCSSClass(s.type) }) ), { oldNumber: s.oldNumber, newNumber: s.newNumber } ) : void 0, f = void 0 !== l && void 0 !== l.newNumber ? Object.assign( Object.assign( {}, void 0 !== c ? { prefix: c.newLine.prefix, content: c.newLine.content, type: a.CSSLineClass.INSERT_CHANGES } : Object.assign(Object.assign({}, a.deconstructLine(l.content, t)), { type: a.toCSSClass(l.type) }) ), { oldNumber: l.oldNumber, newNumber: l.newNumber } ) : void 0, { left: u, right: h } = this.generateLineHtml(e, d, f); (r.left += u), (r.right += h); } return r; } generateLineHtml(e, t, n) { return { left: this.generateSingleLineHtml(e, t), right: this.generateSingleLineHtml(e, n) }; } generateSingleLineHtml(e, t) { if (void 0 === t) return ''; const n = this.hoganUtils.render(d, 'numbers', { oldNumber: t.oldNumber || '', newNumber: t.newNumber || '' }); return this.hoganUtils.render(c, 'line', { type: t.type, lineClass: 'd2h-code-linenumber', contentClass: 'd2h-code-line', prefix: ' ' === t.prefix ? ' ' : t.prefix, content: t.content, lineNumber: n, line: t, file: e }); } }; }, 483: (e, t) => { 'use strict'; function n(e, t) { if (0 === e.length) return t.length; if (0 === t.length) return e.length; const n = []; let i, r; for (i = 0; i <= t.length; i++) n[i] = [i]; for (r = 0; r <= e.length; r++) n[0][r] = r; for (i = 1; i <= t.length; i++) for (r = 1; r <= e.length; r++) t.charAt(i - 1) === e.charAt(r - 1) ? (n[i][r] = n[i - 1][r - 1]) : (n[i][r] = Math.min(n[i - 1][r - 1] + 1, Math.min(n[i][r - 1] + 1, n[i - 1][r] + 1))); return n[t.length][e.length]; } Object.defineProperty(t, '__esModule', { value: !0 }), (t.newMatcherFn = t.newDistanceFn = t.levenshtein = void 0), (t.levenshtein = n), (t.newDistanceFn = function (e) { return (t, i) => { const r = e(t).trim(), s = e(i).trim(); return n(r, s) / (r.length + s.length); }; }), (t.newMatcherFn = function (e) { return function t(n, i, r = 0, s = new Map()) { const o = (function (t, n, i = new Map()) { let r, s = 1 / 0; for (let o = 0; o < t.length; ++o) for (let a = 0; a < n.length; ++a) { const l = JSON.stringify([t[o], n[a]]); let c; (i.has(l) && (c = i.get(l))) || ((c = e(t[o], n[a])), i.set(l, c)), c < s && ((s = c), (r = { indexA: o, indexB: a, score: s })); } return r; })(n, i, s); if (!o || n.length + i.length < 3) return [[n, i]]; const a = n.slice(0, o.indexA), l = i.slice(0, o.indexB), c = [n[o.indexA]], d = [i[o.indexB]], f = o.indexA + 1, u = o.indexB + 1, h = n.slice(f), p = i.slice(u), b = t(a, l, r + 1, s), g = t(c, d, r + 1, s), m = t(h, p, r + 1, s); let v = g; return ( (o.indexA > 0 || o.indexB > 0) && (v = b.concat(v)), (n.length > f || i.length > u) && (v = v.concat(m)), v ); }; }); }, 741: function (e, t, n) { 'use strict'; var i = (this && this.__createBinding) || (Object.create ? function (e, t, n, i) { void 0 === i && (i = n); var r = Object.getOwnPropertyDescriptor(t, n); (r && !('get' in r ? !t.__esModule : r.writable || r.configurable)) || (r = { enumerable: !0, get: function () { return t[n]; } }), Object.defineProperty(e, i, r); } : function (e, t, n, i) { void 0 === i && (i = n), (e[i] = t[n]); }), r = (this && this.__setModuleDefault) || (Object.create ? function (e, t) { Object.defineProperty(e, 'default', { enumerable: !0, value: t }); } : function (e, t) { e.default = t; }), s = (this && this.__importStar) || function (e) { if (e && e.__esModule) return e; var t = {}; if (null != e) for (var n in e) 'default' !== n && Object.prototype.hasOwnProperty.call(e, n) && i(t, e, n); return r(t, e), t; }; Object.defineProperty(t, '__esModule', { value: !0 }), (t.diffHighlight = t.getFileIcon = t.getHtmlId = t.filenameDiff = t.deconstructLine = t.escapeForHtml = t.colorSchemeToCss = t.toCSSClass = t.defaultRenderConfig = t.CSSLineClass = void 0); const o = s(n(785)), a = n(593), l = s(n(483)), c = n(699); (t.CSSLineClass = { INSERTS: 'd2h-ins', DELETES: 'd2h-del', CONTEXT: 'd2h-cntx', INFO: 'd2h-info', INSERT_CHANGES: 'd2h-ins d2h-change', DELETE_CHANGES: 'd2h-del d2h-change' }), (t.defaultRenderConfig = { matching: c.LineMatchingType.NONE, matchWordsThreshold: 0.25, maxLineLengthHighlight: 1e4, diffStyle: c.DiffStyleType.WORD, colorScheme: c.ColorSchemeType.LIGHT }); const d = '/', f = l.newDistanceFn((e) => e.value), u = l.newMatcherFn(f); function h(e) { return -1 !== e.indexOf('dev/null'); } function p(e) { return e.replace(/(]*>((.|\n)*?)<\/del>)/g, ''); } function b(e) { return e .slice(0) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, ''') .replace(/\//g, '/'); } function g(e, t, n = !0) { const i = (function (e) { return e ? 2 : 1; })(t); return { prefix: e.substring(0, i), content: n ? b(e.substring(i)) : e.substring(i) }; } function m(e) { const t = (0, a.unifyPath)(e.oldName), n = (0, a.unifyPath)(e.newName); if (t === n || h(t) || h(n)) return h(n) ? t : n; { const e = [], i = [], r = t.split(d), s = n.split(d); let o = 0, a = r.length - 1, l = s.length - 1; for (; o < a && o < l && r[o] === s[o]; ) e.push(s[o]), (o += 1); for (; a > o && l > o && r[a] === s[l]; ) i.unshift(s[l]), (a -= 1), (l -= 1); const c = e.join(d), f = i.join(d), u = r.slice(o, a + 1).join(d), h = s.slice(o, l + 1).join(d); return c.length && f.length ? c + d + '{' + u + ' → ' + h + '}' + d + f : c.length ? c + d + '{' + u + ' → ' + h + '}' : f.length ? '{' + u + ' → ' + h + '}' + d + f : t + ' → ' + n; } } (t.toCSSClass = function (e) { switch (e) { case c.LineType.CONTEXT: return t.CSSLineClass.CONTEXT; case c.LineType.INSERT: return t.CSSLineClass.INSERTS; case c.LineType.DELETE: return t.CSSLineClass.DELETES; } }), (t.colorSchemeToCss = function (e) { switch (e) { case c.ColorSchemeType.DARK: return 'd2h-dark-color-scheme'; case c.ColorSchemeType.AUTO: return 'd2h-auto-color-scheme'; case c.ColorSchemeType.LIGHT: default: return 'd2h-light-color-scheme'; } }), (t.escapeForHtml = b), (t.deconstructLine = g), (t.filenameDiff = m), (t.getHtmlId = function (e) { return `d2h-${(0, a.hashCode)(m(e)).toString().slice(-6)}`; }), (t.getFileIcon = function (e) { let t = 'file-changed'; return ( e.isRename || e.isCopy ? (t = 'file-renamed') : e.isNew ? (t = 'file-added') : e.isDeleted ? (t = 'file-deleted') : e.newName !== e.oldName && (t = 'file-renamed'), t ); }), (t.diffHighlight = function (e, n, i, r = {}) { const { matching: s, maxLineLengthHighlight: a, matchWordsThreshold: l, diffStyle: c } = Object.assign(Object.assign({}, t.defaultRenderConfig), r), d = g(e, i, !1), h = g(n, i, !1); if (d.content.length > a || h.content.length > a) return { oldLine: { prefix: d.prefix, content: b(d.content) }, newLine: { prefix: h.prefix, content: b(h.content) } }; const m = 'char' === c ? o.diffChars(d.content, h.content) : o.diffWordsWithSpace(d.content, h.content), v = []; if ('word' === c && 'words' === s) { const e = m.filter((e) => e.removed), t = m.filter((e) => e.added); u(t, e).forEach((e) => { 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])); }); } const y = m.reduce((e, t) => { const n = t.added ? 'ins' : t.removed ? 'del' : null, i = v.indexOf(t) > -1 ? ' class="d2h-change"' : '', r = b(t.value); return null !== n ? `${e}<${n}${i}>${r}` : `${e}${r}`; }, ''); return { oldLine: { prefix: d.prefix, content: ((w = y), w.replace(/(]*>((.|\n)*?)<\/ins>)/g, '')) }, newLine: { prefix: h.prefix, content: p(y) } }; var w; }); }, 170: function (e, t, n) { 'use strict'; var i = (this && this.__createBinding) || (Object.create ? function (e, t, n, i) { void 0 === i && (i = n); var r = Object.getOwnPropertyDescriptor(t, n); (r && !('get' in r ? !t.__esModule : r.writable || r.configurable)) || (r = { enumerable: !0, get: function () { return t[n]; } }), Object.defineProperty(e, i, r); } : function (e, t, n, i) { void 0 === i && (i = n), (e[i] = t[n]); }), r = (this && this.__setModuleDefault) || (Object.create ? function (e, t) { Object.defineProperty(e, 'default', { enumerable: !0, value: t }); } : function (e, t) { e.default = t; }), s = (this && this.__importStar) || function (e) { if (e && e.__esModule) return e; var t = {}; if (null != e) for (var n in e) 'default' !== n && Object.prototype.hasOwnProperty.call(e, n) && i(t, e, n); return r(t, e), t; }; Object.defineProperty(t, '__esModule', { value: !0 }), (t.defaultSideBySideRendererConfig = void 0); const o = s(n(483)), a = s(n(741)), l = n(699); t.defaultSideBySideRendererConfig = Object.assign(Object.assign({}, a.defaultRenderConfig), { renderNothingWhenEmpty: !1, matchingMaxComparisons: 2500, maxLineSizeInBlockForComparison: 200 }); const c = 'generic'; t.default = class { constructor(e, n = {}) { (this.hoganUtils = e), (this.config = Object.assign(Object.assign({}, t.defaultSideBySideRendererConfig), n)); } render(e) { const t = e .map((e) => { let t; return ( (t = e.blocks.length ? this.generateFileHtml(e) : this.generateEmptyDiff()), this.makeFileDiffHtml(e, t) ); }) .join('\n'); return this.hoganUtils.render(c, 'wrapper', { colorScheme: a.colorSchemeToCss(this.config.colorScheme), content: t }); } makeFileDiffHtml(e, t) { if (this.config.renderNothingWhenEmpty && Array.isArray(e.blocks) && 0 === e.blocks.length) return ''; const n = this.hoganUtils.template('side-by-side', 'file-diff'), i = this.hoganUtils.template(c, 'file-path'), r = this.hoganUtils.template('icon', 'file'), s = this.hoganUtils.template('tag', a.getFileIcon(e)); return n.render({ file: e, fileHtmlId: a.getHtmlId(e), diffs: t, filePath: i.render({ fileDiffName: a.filenameDiff(e) }, { fileIcon: r, fileTag: s }) }); } generateEmptyDiff() { return { right: '', left: this.hoganUtils.render(c, 'empty-diff', { contentClass: 'd2h-code-side-line', CSSLineClass: a.CSSLineClass }) }; } generateFileHtml(e) { const t = o.newMatcherFn(o.newDistanceFn((t) => a.deconstructLine(t.content, e.isCombined).content)); return e.blocks .map((n) => { const i = { left: this.makeHeaderHtml(n.header, e), right: this.makeHeaderHtml('') }; return ( this.applyLineGroupping(n).forEach(([n, r, s]) => { if (r.length && s.length && !n.length) this.applyRematchMatching(r, s, t).map(([t, n]) => { const { left: r, right: s } = this.processChangedLines(e.isCombined, t, n); (i.left += r), (i.right += s); }); else if (n.length) n.forEach((t) => { const { prefix: n, content: r } = a.deconstructLine(t.content, e.isCombined), { left: s, right: o } = this.generateLineHtml( { type: a.CSSLineClass.CONTEXT, prefix: n, content: r, number: t.oldNumber }, { type: a.CSSLineClass.CONTEXT, prefix: n, content: r, number: t.newNumber } ); (i.left += s), (i.right += o); }); else if (r.length || s.length) { const { left: t, right: n } = this.processChangedLines(e.isCombined, r, s); (i.left += t), (i.right += n); } else console.error('Unknown state reached while processing groups of lines', n, r, s); }), i ); }) .reduce((e, t) => ({ left: e.left + t.left, right: e.right + t.right }), { left: '', right: '' }); } applyLineGroupping(e) { const t = []; let n = [], i = []; for (let r = 0; r < e.lines.length; r++) { const s = e.lines[r]; ((s.type !== l.LineType.INSERT && i.length) || (s.type === l.LineType.CONTEXT && n.length > 0)) && (t.push([[], n, i]), (n = []), (i = [])), s.type === l.LineType.CONTEXT ? t.push([[s], [], []]) : s.type === l.LineType.INSERT && 0 === n.length ? t.push([[], [], [s]]) : s.type === l.LineType.INSERT && n.length > 0 ? i.push(s) : s.type === l.LineType.DELETE && n.push(s); } return (n.length || i.length) && (t.push([[], n, i]), (n = []), (i = [])), t; } applyRematchMatching(e, t, n) { const i = e.length * t.length, r = Math.max.apply(null, [0].concat(e.concat(t).map((e) => e.content.length))); return i < this.config.matchingMaxComparisons && r < this.config.maxLineSizeInBlockForComparison && ('lines' === this.config.matching || 'words' === this.config.matching) ? n(e, t) : [[e, t]]; } makeHeaderHtml(e, t) { return this.hoganUtils.render(c, 'block-header', { CSSLineClass: a.CSSLineClass, blockHeader: (null == t ? void 0 : t.isTooBig) ? e : a.escapeForHtml(e), lineClass: 'd2h-code-side-linenumber', contentClass: 'd2h-code-side-line' }); } processChangedLines(e, t, n) { const i = { right: '', left: '' }, r = Math.max(t.length, n.length); for (let s = 0; s < r; s++) { const r = t[s], o = n[s], l = void 0 !== r && void 0 !== o ? a.diffHighlight(r.content, o.content, e, this.config) : void 0, c = void 0 !== r && void 0 !== r.oldNumber ? Object.assign( Object.assign( {}, void 0 !== l ? { prefix: l.oldLine.prefix, content: l.oldLine.content, type: a.CSSLineClass.DELETE_CHANGES } : Object.assign(Object.assign({}, a.deconstructLine(r.content, e)), { type: a.toCSSClass(r.type) }) ), { number: r.oldNumber } ) : void 0, d = void 0 !== o && void 0 !== o.newNumber ? Object.assign( Object.assign( {}, void 0 !== l ? { prefix: l.newLine.prefix, content: l.newLine.content, type: a.CSSLineClass.INSERT_CHANGES } : Object.assign(Object.assign({}, a.deconstructLine(o.content, e)), { type: a.toCSSClass(o.type) }) ), { number: o.newNumber } ) : void 0, { left: f, right: u } = this.generateLineHtml(c, d); (i.left += f), (i.right += u); } return i; } generateLineHtml(e, t) { return { left: this.generateSingleHtml(e), right: this.generateSingleHtml(t) }; } generateSingleHtml(e) { const t = 'd2h-code-side-linenumber', n = 'd2h-code-side-line'; return this.hoganUtils.render(c, 'line', { type: (null == e ? void 0 : e.type) || `${a.CSSLineClass.CONTEXT} d2h-emptyplaceholder`, lineClass: void 0 !== e ? t : `${t} d2h-code-side-emptyplaceholder`, contentClass: void 0 !== e ? n : `${n} d2h-code-side-emptyplaceholder`, prefix: ' ' === (null == e ? void 0 : e.prefix) ? ' ' : null == e ? void 0 : e.prefix, content: null == e ? void 0 : e.content, lineNumber: null == e ? void 0 : e.number }); } }; }, 699: (e, t) => { 'use strict'; var n, i; Object.defineProperty(t, '__esModule', { value: !0 }), (t.ColorSchemeType = t.DiffStyleType = t.LineMatchingType = t.OutputFormatType = t.LineType = void 0), (function (e) { (e.INSERT = 'insert'), (e.DELETE = 'delete'), (e.CONTEXT = 'context'); })(n || (t.LineType = n = {})), (t.OutputFormatType = { LINE_BY_LINE: 'line-by-line', SIDE_BY_SIDE: 'side-by-side' }), (t.LineMatchingType = { LINES: 'lines', WORDS: 'words', NONE: 'none' }), (t.DiffStyleType = { WORD: 'word', CHAR: 'char' }), (function (e) { (e.AUTO = 'auto'), (e.DARK = 'dark'), (e.LIGHT = 'light'); })(i || (t.ColorSchemeType = i = {})); }, 593: (e, t) => { 'use strict'; Object.defineProperty(t, '__esModule', { value: !0 }), (t.hashCode = t.unifyPath = t.escapeForRegExp = void 0); const n = RegExp( '[' + ['-', '[', ']', '/', '{', '}', '(', ')', '*', '+', '?', '.', '\\', '^', '$', '|'].join('\\') + ']', 'g' ); (t.escapeForRegExp = function (e) { return e.replace(n, '\\$&'); }), (t.unifyPath = function (e) { return e ? e.replace(/\\/g, '/') : e; }), (t.hashCode = function (e) { let t, n, i, r = 0; for (t = 0, i = e.length; t < i; t++) (n = e.charCodeAt(t)), (r = (r << 5) - r + n), (r |= 0); return r; }); } }), (t = {}), (function n(i) { var r = t[i]; if (void 0 !== r) return r.exports; var s = (t[i] = { exports: {} }); return e[i].call(s.exports, s, s.exports, n), s.exports; })(834) ); var e, t; }); ================================================ FILE: packages/bruno-app/public/theme/dark.js ================================================ const darkTheme = { 'brand': '#546de5', 'text': 'rgb(52 52 52)', 'primary-text': '#ffffff', 'primary-theme': '#1e1e1e', 'secondary-text': '#929292', 'sidebar-collection-item-active-indent-border': '#d0d0d0', 'sidebar-collection-item-active-background': '#e1e1e1', 'sidebar-background': '#252526', 'sidebar-bottom-bg': '#68217a', 'request-dragbar-background': '#efefef', 'request-dragbar-background-active': 'rgb(200, 200, 200)', 'tab-inactive': 'rgb(155 155 155)', 'tab-active-border': '#546de5', 'layout-border': '#dedede', 'codemirror-border': '#efefef', 'codemirror-background': 'rgb(243, 243, 243)', 'text-link': '#1663bb', 'text-danger': 'rgb(185, 28, 28)', 'background-danger': '#dc3545', 'method-get': 'rgb(5, 150, 105)', 'method-post': '#8e44ad', 'method-delete': 'rgb(185, 28, 28)', 'method-patch': 'rgb(52 52 52)', 'method-options': 'rgb(52 52 52)', 'method-head': 'rgb(52 52 52)', 'table-stripe': '#f3f3f3' }; export default darkTheme; ================================================ FILE: packages/bruno-app/public/theme/index.js ================================================ import darkTheme from './dark'; import lightTheme from './light'; export default { Light: lightTheme, Dark: darkTheme }; ================================================ FILE: packages/bruno-app/public/theme/light.js ================================================ const lightTheme = { 'brand': '#546de5', 'text': 'rgb(52 52 52)', 'primary-text': 'rgb(52 52 52)', 'primary-theme': '#ffffff', 'secondary-text': '#929292', 'sidebar-collection-item-active-indent-border': '#d0d0d0', 'sidebar-collection-item-active-background': '#e1e1e1', 'sidebar-background': '#f3f3f3', 'sidebar-bottom-bg': '#f3f3f3', 'request-dragbar-background': '#efefef', 'request-dragbar-background-active': 'rgb(200, 200, 200)', 'tab-inactive': 'rgb(155 155 155)', 'tab-active-border': '#546de5', 'layout-border': '#dedede', 'codemirror-border': '#efefef', 'codemirror-background': 'rgb(243, 243, 243)', 'text-link': '#1663bb', 'text-danger': 'rgb(185, 28, 28)', 'background-danger': '#dc3545', 'method-get': 'rgb(5, 150, 105)', 'method-post': '#8e44ad', 'method-delete': 'rgb(185, 28, 28)', 'method-patch': 'rgb(52 52 52)', 'method-options': 'rgb(52 52 52)', 'method-head': 'rgb(52 52 52)', 'table-stripe': '#f3f3f3' }; export default lightTheme; ================================================ FILE: packages/bruno-app/rsbuild.config.mjs ================================================ import { defineConfig } from '@rsbuild/core'; import { pluginReact } from '@rsbuild/plugin-react'; import { pluginBabel } from '@rsbuild/plugin-babel'; import { pluginStyledComponents } from '@rsbuild/plugin-styled-components'; import { pluginSass } from '@rsbuild/plugin-sass'; import { pluginNodePolyfill } from '@rsbuild/plugin-node-polyfill' export default defineConfig({ plugins: [ pluginNodePolyfill(), pluginReact(), pluginStyledComponents(), pluginSass(), pluginBabel({ include: /\.(?:js|jsx|tsx)$/, babelLoaderOptions(opts) { opts.plugins?.unshift('babel-plugin-react-compiler'); } }) ], source: { tsconfigPath: './jsconfig.json', // Specifies the path to the JavaScript/TypeScript configuration file, exclude: [ '**/test-utils/**', '**/*.test.*', '**/*.spec.*' ] }, html: { title: 'Bruno' }, tools: { rspack: { module: { parser: { javascript: { // This loads the JavaScript contents from a library along with the main JavaScript bundle. dynamicImportMode: "eager", }, }, }, ignoreWarnings: [ (warning) => warning.message.includes('Critical dependency: the request of a dependency is an expression') && warning?.moduleDescriptor?.name?.includes('flow-parser') ], // Add externals configuration to exclude Node.js libraries externals: { // List specific Node.js modules you want to exclude // Format: 'module-name': 'commonjs module-name' 'worker_threads': 'commonjs worker_threads', // 'path': 'commonjs path' } }, } }); ================================================ FILE: packages/bruno-app/src/components/Accordion/index.js ================================================ import React, { createContext, useContext, useState } from 'react'; import { IconChevronDown } from '@tabler/icons'; import { AccordionItem, AccordionHeader, AccordionContent } from './styledWrapper'; const AccordionContext = createContext(); const Accordion = ({ children, defaultIndex, dataTestId }) => { const [openIndex, setOpenIndex] = useState(defaultIndex); const toggleItem = (index) => { setOpenIndex(openIndex === index ? null : index); }; return (
    {children}
    ); }; const Item = ({ index, children, ...props }) => { return ( {React.Children.map(children, (child) => React.cloneElement(child, { index }))} ); }; export const Header = ({ index, children, ...props }) => { const { openIndex, toggleItem } = useContext(AccordionContext); const isOpen = openIndex === index; return ( toggleItem(index)} {...props} className={isOpen ? 'open' : ''}>
    {children}
    ); }; const Content = ({ index, children, ...props }) => { const { openIndex } = useContext(AccordionContext); const isOpen = openIndex === index; return ( {children} ); }; Accordion.Item = Item; Accordion.Header = Header; Accordion.Content = Content; export default Accordion; ================================================ FILE: packages/bruno-app/src/components/Accordion/styledWrapper.js ================================================ import styled from 'styled-components'; const AccordionItem = styled.div` border: 1px solid ${(props) => props.theme.input.border}; border-radius: 4px; overflow: hidden; margin-bottom: 1rem; `; const AccordionHeader = styled.button` width: 100%; display: flex; padding: 0.75rem 1rem; background: transparent; cursor: pointer; font-weight: 500; &.open, &:hover { background-color: ${(props) => props.theme.plainGrid.hoverBg}; } `; const AccordionContent = styled.div` padding: ${(props) => (props.isOpen ? '1rem' : '0')}; max-height: ${(props) => (props.isOpen ? 'auto' : '0')}; `; export { AccordionItem, AccordionHeader, AccordionContent }; ================================================ FILE: packages/bruno-app/src/components/ApiSpecPanel/FileEditor/CodeEditor/Plugins/Yaml/index.js ================================================ const yamlPlugin = (cm) => { cm.defineMode('yaml', function () { var cons = ['true', 'false', 'on', 'off', 'yes', 'no']; var keywordRegex = new RegExp('\\b((' + cons.join(')|(') + '))$', 'i'); return { token: function (stream, state) { var ch = stream.peek(); var esc = state.escaped; state.escaped = false; /* comments */ if (ch == '#' && (stream.pos == 0 || /\s/.test(stream.string.charAt(stream.pos - 1)))) { stream.skipToEnd(); return 'comment'; } if (stream.match(/^('([^']|\\.)*'?|"([^"]|\\.)*"?)/)) return 'string'; if (state.literal && stream.indentation() > state.keyCol) { stream.skipToEnd(); return 'string'; } else if (state.literal) { state.literal = false; } if (stream.sol()) { state.keyCol = 0; state.pair = false; state.pairStart = false; /* document start */ if (stream.match('---')) { return 'def'; } /* document end */ if (stream.match('...')) { return 'def'; } /* array list item */ if (stream.match(/\s*-\s+/)) { return 'meta'; } } /* inline pairs/lists */ if (stream.match(/^(\{|\}|\[|\])/)) { if (ch == '{') state.inlinePairs++; else if (ch == '}') state.inlinePairs--; else if (ch == '[') state.inlineList++; else state.inlineList--; return 'meta'; } /* list separator */ if (state.inlineList > 0 && !esc && ch == ',') { stream.next(); return 'meta'; } /* pairs separator */ if (state.inlinePairs > 0 && !esc && ch == ',') { state.keyCol = 0; state.pair = false; state.pairStart = false; stream.next(); return 'meta'; } /* start of value of a pair */ if (state.pairStart) { /* block literals */ if (stream.match(/^\s*(\||\>)\s*/)) { state.literal = true; return 'meta'; } /* references */ if (stream.match(/^\s*(\&|\*)[a-z0-9\._-]+\b/i)) { return 'variable-2'; } /* numbers */ if (state.inlinePairs == 0 && stream.match(/^\s*-?[0-9\.\,]+\s?$/)) { return 'number'; } if (state.inlinePairs > 0 && stream.match(/^\s*-?[0-9\.\,]+\s?(?=(,|}))/)) { return 'number'; } /* keywords */ if (stream.match(keywordRegex)) { return 'keyword'; } } /* pairs (associative arrays) -> key */ if ( !state.pair && stream.match(/^\s*(?:[,\[\]{}&*!|>'"%@`][^\s'":]|[^\s,\[\]{}#&*!|>'"%@`])[^#:]*(?=:($|\s))/) ) { state.pair = true; state.keyCol = stream.indentation(); return 'atom'; } if (state.pair && stream.match(/^:\s*/)) { state.pairStart = true; return 'meta'; } /* nothing found, continue */ state.pairStart = false; state.escaped = ch == '\\'; stream.next(); return null; }, startState: function () { return { pair: false, pairStart: false, keyCol: 0, inlinePairs: 0, inlineList: 0, literal: false, escaped: false }; }, lineComment: '#', fold: 'indent' }; }); cm.defineMIME('text/x-yaml', 'yaml'); cm.defineMIME('text/yaml', 'yaml'); }; export default yamlPlugin; ================================================ FILE: packages/bruno-app/src/components/ApiSpecPanel/FileEditor/CodeEditor/StyledWrapper.js ================================================ import styled from 'styled-components'; const StyledWrapper = styled.div` div.CodeMirror { height: calc(100vh - 9rem); background: ${(props) => props.theme.codemirror.bg}; border: solid 1px ${(props) => props.theme.codemirror.border}; font-family: ${(props) => (props.font ? props.font : 'default')}; font-size: ${(props) => props.theme.font.size.base}; line-break: anywhere; } .CodeMirror-dialog { overflow: visible; input { background: transparent; border: 1px solid #d3d6db; outline: none; border-radius: 0px; } } .CodeMirror-overlayscroll-horizontal div, .CodeMirror-overlayscroll-vertical div { background: #d2d7db; } textarea.cm-editor { position: relative; } // Todo: dark mode temporary fix // Clean this .CodeMirror.cm-s-monokai { .CodeMirror-overlayscroll-horizontal div, .CodeMirror-overlayscroll-vertical div { background: #444444; } } .cm-s-monokai span.cm-property, .cm-s-monokai span.cm-attribute { color: #9cdcfe !important; } .cm-s-monokai span.cm-string { color: #ce9178 !important; } .cm-s-monokai span.cm-number { color: #b5cea8 !important; } .cm-s-monokai span.cm-atom { color: #569cd6 !important; } .cm-variable-valid { color: ${(props) => props.theme.codemirror.variable.valid}; } .cm-variable-invalid { color: ${(props) => props.theme.codemirror.variable.invalid}; } `; export default StyledWrapper; ================================================ FILE: packages/bruno-app/src/components/ApiSpecPanel/FileEditor/CodeEditor/index.js ================================================ /** * Copyright (c) 2021 GraphQL Contributors. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ import React from 'react'; import StyledWrapper from './StyledWrapper'; import yamlPlugin from './Plugins/Yaml/index'; let CodeMirror; const SERVER_RENDERED = typeof window === 'undefined' || global['PREVENT_CODEMIRROR_RENDER'] === true; if (!SERVER_RENDERED) { CodeMirror = require('codemirror'); } export default class CodeEditor extends React.Component { constructor(props) { super(props); this.cachedValue = props.value || ''; this.variables = {}; this.lintOptions = { esversion: 11, expr: true, asi: true }; } componentWillMount() { switch (this.props.mode) { case 'yaml': // YAML linting and hightlighting plugin yamlPlugin(CodeMirror); break; default: break; } } componentDidMount() { const editor = (this.editor = CodeMirror(this._node, { value: this.props.value || '', lineNumbers: true, lineWrapping: true, tabSize: 2, mode: this.props.mode || 'application/text', keyMap: 'sublime', autoCloseBrackets: true, matchBrackets: true, showCursorWhenSelecting: true, foldGutter: true, gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter', 'CodeMirror-lint-markers'], lint: this.lintOptions, readOnly: this.props.readOnly, scrollbarStyle: 'overlay', theme: this.props.theme === 'dark' ? 'monokai' : 'default', extraKeys: { 'Cmd-S': () => { if (this.props.onSave) { this.props.onSave(); } }, 'Ctrl-S': () => { if (this.props.onSave) { this.props.onSave(); } }, 'Cmd-F': 'findPersistent', 'Ctrl-F': 'findPersistent', 'Cmd-H': 'replace', 'Ctrl-H': 'replace', 'Tab': function (cm) { cm.getSelection().includes('\n') || editor.getLine(cm.getCursor().line) == cm.getSelection() ? cm.execCommand('indentMore') : cm.replaceSelection(' ', 'end'); }, 'Shift-Tab': 'indentLess', 'Ctrl-Space': 'autocomplete', 'Cmd-Space': 'autocomplete', 'Ctrl-Y': 'foldAll', 'Cmd-Y': 'foldAll', 'Ctrl-I': 'unfoldAll', 'Cmd-I': 'unfoldAll' } })); if (editor) { editor.setOption('lint', this.props.mode && editor.getValue().trim().length > 0 ? this.lintOptions : false); editor.on('change', this._onEdit); } } componentDidUpdate(prevProps) { this.ignoreChangeEvent = true; if (this.props.value !== prevProps.value && this.props.value !== this.cachedValue && this.editor) { this.cachedValue = this.props.value; this.editor.setValue(this.props.value); } if (this.props.theme !== prevProps.theme && this.editor) { this.editor.setOption('theme', this.props.theme === 'dark' ? 'monokai' : 'default'); } this.ignoreChangeEvent = false; } componentWillUnmount() { if (this.editor) { this.editor.off('change', this._onEdit); this.editor = null; } } render() { if (this.editor) { this.editor.refresh(); } return ( { this._node = node; }} /> ); } _onEdit = () => { if (!this.ignoreChangeEvent && this.editor) { this.editor.setOption('lint', this.editor.getValue().trim().length > 0 ? this.lintOptions : false); this.cachedValue = this.editor.getValue(); if (this.props.onEdit) { this.props.onEdit(this.cachedValue); } } }; } ================================================ FILE: packages/bruno-app/src/components/ApiSpecPanel/Renderers/Swagger/StyledWrapper.js ================================================ import styled from 'styled-components'; const StyledWrapper = styled.div` .swagger-root { height: calc(100vh - 7rem); border-left: solid 1px ${(props) => props.theme.border.border1}; overflow-y: auto; background: ${(props) => props.theme.bg}; padding-bottom: 20px; /* ── Global reset ── */ .swagger-ui { font-family: inherit; font-size: ${(props) => props.theme.font.size.base}; color: ${(props) => props.theme.text}; * { border-color: ${(props) => props.theme.border.border1}; } .auth-container { padding: 0; } select { box-shadow: none !important; } .wrapper { padding: 0 20px; max-width: none; } /* ── Info section ── */ .info { margin: 16px 0 12px; hgroup.main { margin: 0; } .title { font-size: 16px; font-weight: 600; color: ${(props) => props.theme.text}; small { padding: 2px 6px !important; font-size: 10px; vertical-align: middle; border-radius: 3px; pre { color: ${(props) => props.theme.text} !important; font-size: 10px; } } } .base-url { font-size: ${(props) => props.theme.font.size.xs}; color: ${(props) => props.theme.colors.text.muted}; } .description { font-size: ${(props) => props.theme.font.size.sm}; color: ${(props) => props.theme.colors.text.muted}; p, li { font-size: ${(props) => props.theme.font.size.sm}; color: ${(props) => props.theme.colors.text.muted}; margin: 3px 0; line-height: 1.5; } h1, h2, h3, h4, h5, h6 { color: ${(props) => props.theme.text}; } a { color: ${(props) => props.theme.textLink}; } } } /* Version / OAS badges */ .version-stamp span.version { background: ${(props) => props.theme.border.border1} !important; border: 1px solid ${(props) => props.theme.colors.text.muted} !important; color: ${(props) => props.theme.text} !important; font-size: 9px; padding: 2px 6px; border-radius: 3px; } .version-pragma { font-size: ${(props) => props.theme.font.size.xs}; color: ${(props) => props.theme.colors.text.muted}; } /* ── Tag section headings ── */ .opblock-tag-section { .opblock-tag { font-size: ${(props) => props.theme.font.size.md}; color: ${(props) => props.theme.text}; border-bottom: none; padding: 0; &:hover { background: ${(props) => props.theme.background.mantle}; } a { color: ${(props) => props.theme.text} !important; } small { font-size: ${(props) => props.theme.font.size.xs}; color: ${(props) => props.theme.colors.text.muted}; padding: 0 10px; } } } /* ── Operation blocks (GET, POST, PUT, DELETE, PATCH) ── */ .opblock { margin: 0 0 8px; border-radius: 4px; border: 1px solid ${(props) => props.theme.border.border1} !important; background: ${(props) => props.theme.bg} !important; box-shadow: none !important; .opblock-summary { padding: 6px 10px; border: none !important; background: transparent !important; .opblock-summary-method { font-size: 10px; font-weight: 700; padding: 3px 8px; min-width: 50px; text-align: center; border-radius: 3px; } .opblock-summary-path { font-size: ${(props) => props.theme.font.size.sm}; a, span { color: ${(props) => props.theme.text} !important; } } .opblock-summary-description { font-size: ${(props) => props.theme.font.size.xs}; color: ${(props) => props.theme.colors.text.muted}; } .opblock-summary-control { svg { fill: ${(props) => props.theme.colors.text.muted}; width: 14px; height: 14px; } } } .opblock-body { font-size: ${(props) => props.theme.font.size.sm}; color: ${(props) => props.theme.text}; background: ${(props) => props.theme.bg}; border-top: 1px solid ${(props) => props.theme.border.border1}; .opblock-description-wrapper, .opblock-section { p { color: ${(props) => props.theme.colors.text.muted}; font-size: ${(props) => props.theme.font.size.sm}; } } .tab-header .tab-item { color: ${(props) => props.theme.colors.text.muted}; &.active { color: ${(props) => props.theme.text}; } } select { color: ${(props) => props.theme.text}; background: ${(props) => props.theme.bg}; border: 1px solid ${(props) => props.theme.border.border1}; border-radius: 3px; font-size: ${(props) => props.theme.font.size.xs}; padding: 2px 6px; } input[type="text"] { color: ${(props) => props.theme.text}; background: ${(props) => props.theme.bg}; border: 1px solid ${(props) => props.theme.border.border1}; border-radius: 3px; font-size: ${(props) => props.theme.font.size.sm}; } } } /* Method badge colors — keep them but tone down */ .opblock.opblock-get .opblock-summary-method { background: #61affe; color: #fff; } .opblock.opblock-post .opblock-summary-method { background: #49cc90; color: #fff; } .opblock.opblock-put .opblock-summary-method { background: #fca130; color: #fff; } .opblock.opblock-delete .opblock-summary-method { background: #f93e3e; color: #fff; } .opblock.opblock-patch .opblock-summary-method { background: #50e3c2; color: #000; } /* Lock / authorization icons */ .authorization__btn { svg { fill: ${(props) => props.theme.colors.text.muted}; width: 14px; height: 14px; } } /* ── Tables ── */ table { font-size: ${(props) => props.theme.font.size.sm}; thead { tr { th { font-size: ${(props) => props.theme.font.size.xs} !important; color: ${(props) => props.theme.colors.text.muted} !important; border-bottom: 1px solid ${(props) => props.theme.border.border1} !important; padding: 6px 0; } } } td { padding: 6px 0; border-bottom: 1px solid ${(props) => props.theme.border.border1}; color: ${(props) => props.theme.text}; } } .parameter__name { font-size: ${(props) => props.theme.font.size.sm}; color: ${(props) => props.theme.text}; &.required::after { color: ${(props) => props.theme.colors.text.danger || '#c0392b'}; font-size: ${(props) => props.theme.font.size.xs}; } } .parameter__type { font-size: ${(props) => props.theme.font.size.xs}; color: ${(props) => props.theme.colors.text.muted}; } .parameter__in { font-size: ${(props) => props.theme.font.size.xs}; color: ${(props) => props.theme.colors.text.muted}; } /* ── Models / Schemas ── */ section.models { border: 1px solid ${(props) => props.theme.border.border1}; border-radius: 4px; background: ${(props) => props.theme.bg}; padding-bottom: 0px; margin-bottom: 40px; margin-top: 8px; h4 { font-size: ${(props) => props.theme.font.size.sm}; color: ${(props) => props.theme.text}; border-bottom: none; padding: 6px 10px; margin: 0; svg { fill: ${(props) => props.theme.colors.text.muted}; width: 16px; height: 16px; } } .model-container { background: ${(props) => props.theme.bg} !important; margin: 0; padding: 4px 8px; border-bottom: 1px solid ${(props) => props.theme.border.border1}; &:last-child { border-bottom: none; } .model-box { background: ${(props) => props.theme.bg} !important; padding: 2px 0; } } } .model { font-size: 11px; color: ${(props) => props.theme.text}; line-height: 1.4; .prop-type { color: ${(props) => props.theme.textLink}; font-size: 11px; } .prop-format { color: ${(props) => props.theme.colors.text.muted}; font-size: 10px; } span.prop-enum { display: block; color: ${(props) => props.theme.colors.text.muted}; font-size: 10px; } } .model-example { .tab li { color: ${(props) => props.theme.colors.text.muted} !important; } } /* Model expand/collapse toggle */ .model-toggle { cursor: pointer; font-size: 10px; color: ${(props) => props.theme.colors.text.muted}; &::after { color: ${(props) => props.theme.colors.text.muted}; } } /* Model box inner styling */ .model-box { background: ${(props) => props.theme.bg} !important; color: ${(props) => props.theme.text}; } /* Inner model details */ .inner-object { color: ${(props) => props.theme.text}; } /* Model title (schema name) */ .model-title { color: ${(props) => props.theme.text}; font-size: 12px; font-weight: 600; } /* ── JSON Schema 2020-12 (OpenAPI 3.1) schema overrides ── */ .json-schema-2020-12-accordion, .json-schema-2020-12-expand-deep-button, section.models h4 button, .model-box button, .models-control, .opblock-summary, .opblock-summary-control, .opblock-tag { outline: none !important; box-shadow: none !important; } button:focus-visible, .opblock-summary:focus-visible, .opblock-tag:focus-visible, .models-control:focus-visible { outline: 2px solid ${(props) => props.theme.textLink} !important; outline-offset: 2px; } .json-schema-2020-12__title { font-size: 12px !important; font-weight: 600; color: ${(props) => props.theme.text} !important; } .json-schema-2020-12-head { padding: 4px 8px !important; background: ${(props) => props.theme.bg} !important; .json-schema-2020-12-accordion { padding: 0 !important; color: ${(props) => props.theme.text} !important; background: transparent !important; } /* chevron / arrow icon */ .json-schema-2020-12-accordion__icon { fill: ${(props) => props.theme.colors.text.muted} !important; } button.json-schema-2020-12-expand-deep-button { font-size: 10px !important; color: ${(props) => props.theme.colors.text.muted} !important; background: transparent !important; padding: 0 4px !important; } strong.json-schema-2020-12__attribute--primary { font-size: 11px !important; color: ${(props) => props.theme.textLink} !important; font-weight: normal; } } .json-schema-2020-12-body { font-size: 11px !important; margin-left: 16px; color: ${(props) => props.theme.text} !important; .json-schema-2020-12-property { margin-left: 8px; color: ${(props) => props.theme.text} !important; border-color: ${(props) => props.theme.border.border1} !important; } /* property names */ .json-schema-2020-12__title { font-size: 11px !important; font-weight: normal; color: ${(props) => props.theme.text} !important; } /* type badges inside expanded schema */ strong.json-schema-2020-12__attribute--primary { font-size: 10px !important; color: ${(props) => props.theme.textLink} !important; font-weight: normal; } strong.json-schema-2020-12__attribute { font-size: 10px !important; color: ${(props) => props.theme.colors.text.muted} !important; font-weight: normal; } } .json-schema-2020-12 { font-size: 11px !important; margin: 0 !important; width: 100%; height: 100%; color: ${(props) => props.theme.text} !important; background: ${(props) => props.theme.bg} !important; } /* JSON viewer (Examples section inside schema properties) */ .json-schema-2020-12-json-viewer { background: transparent !important; color: ${(props) => props.theme.text} !important; } .json-schema-2020-12-json-viewer__name { color: ${(props) => props.theme.text} !important; } .json-schema-2020-12-json-viewer__name--secondary { color: ${(props) => props.theme.colors.text.muted} !important; font-weight: normal !important; } .json-schema-2020-12-json-viewer__value { color: ${(props) => props.theme.text} !important; } .json-schema-2020-12-json-viewer__value--secondary { color: ${(props) => props.theme.colors.text.subtext0} !important; } .json-schema-2020-12-json-viewer__value--string, .json-schema-2020-12-json-viewer__value--string.json-schema-2020-12-json-viewer__value--secondary { color: ${(props) => props.theme.colors.text.green} !important; } .json-schema-2020-12-json-viewer__value--number, .json-schema-2020-12-json-viewer__value--bigint, .json-schema-2020-12-json-viewer__value--number.json-schema-2020-12-json-viewer__value--secondary, .json-schema-2020-12-json-viewer__value--bigint.json-schema-2020-12-json-viewer__value--secondary { color: ${(props) => props.theme.textLink} !important; } .json-schema-2020-12-json-viewer__value--boolean, .json-schema-2020-12-json-viewer__value--boolean.json-schema-2020-12-json-viewer__value--secondary { color: ${(props) => props.theme.colors.text.warning} !important; } .json-schema-2020-12-json-viewer__value--null, .json-schema-2020-12-json-viewer__value--undefined { color: ${(props) => props.theme.colors.text.muted} !important; } /* enum/keyword example values container */ .json-schema-2020-12-keyword--examples, [data-json-schema-keyword="examples"] { color: ${(props) => props.theme.text} !important; } /* Model collapse/expand all link */ span.model-toggle { color: ${(props) => props.theme.colors.text.muted}; font-size: 10px; } /* Brace styling in models */ .brace-open, .brace-close { color: ${(props) => props.theme.colors.text.muted}; font-size: 11px; } /* ── Code / Response blocks ── */ .microlight { background: ${(props) => props.theme.codemirror.bg} !important; color: ${(props) => props.theme.text} !important; font-size: ${(props) => props.theme.font.size.xs}; border-radius: 4px; padding: 8px; border: 1px solid ${(props) => props.theme.border.border1}; } .highlight-code { background: ${(props) => props.theme.codemirror.bg} !important; > .microlight { border: none; } } pre { color: ${(props) => props.theme.text}; font-size: ${(props) => props.theme.font.size.xs}; border-radius: 4px; } .response-col_status { font-size: ${(props) => props.theme.font.size.sm}; color: ${(props) => props.theme.text}; } .response-col_description { font-size: ${(props) => props.theme.font.size.sm}; color: ${(props) => props.theme.colors.text.muted}; } .responses-inner { h4, h5 { font-size: ${(props) => props.theme.font.size.sm}; color: ${(props) => props.theme.text}; } } /* ── Buttons ── */ .btn { font-size: ${(props) => props.theme.font.size.xs}; border-radius: 4px; box-shadow: none !important; color: ${(props) => props.theme.text}; border-color: ${(props) => props.theme.border.border1}; background: transparent; } .btn.authorize { color: ${(props) => props.theme.text}; border-color: ${(props) => props.theme.border.border1}; background: transparent; svg { fill: ${(props) => props.theme.text}; } span { color: ${(props) => props.theme.text}; } } .btn.execute { background: ${(props) => props.theme.primary?.solid || props.theme.textLink}; color: #fff; border-color: transparent; } .btn-group { .btn { background: ${(props) => props.theme.bg}; color: ${(props) => props.theme.text}; } } /* ── Links ── */ a { color: ${(props) => props.theme.textLink}; } /* ── Servers / Scheme container ── */ .scheme-container { background: ${(props) => props.theme.background.mantle} !important; border-top: 1px solid ${(props) => props.theme.border.border1}; border-bottom: 1px solid ${(props) => props.theme.border.border1}; padding: 10px; box-shadow: none !important; .schemes-title { font-size: ${(props) => props.theme.font.size.sm}; color: ${(props) => props.theme.colors.text.muted}; } label { font-size: ${(props) => props.theme.font.size.sm}; color: ${(props) => props.theme.colors.text.muted}; } select { font-size: ${(props) => props.theme.font.size.sm}; color: ${(props) => props.theme.text}; background: ${(props) => props.theme.bg}; border: 1px solid ${(props) => props.theme.border.border1}; border-radius: 4px; padding: 4px 8px; } } /* ── SVGs / icons ── */ svg { fill: ${(props) => props.theme.colors.text.muted}; } svg.arrow { fill: ${(props) => props.theme.text}; width: 12px; height: 12px; margin-left: 4px; } .expand-operation svg { fill: ${(props) => props.theme.colors.text.muted}; width: 14px; height: 14px; } /* ── Misc / catch-all ── */ .loading-container .loading::after { color: ${(props) => props.theme.colors.text.muted}; font-size: ${(props) => props.theme.font.size.sm}; } .renderedMarkdown p { color: ${(props) => props.theme.colors.text.muted}; font-size: ${(props) => props.theme.font.size.sm}; } .opblock-section-header { background: ${(props) => props.theme.background.mantle} !important; box-shadow: none !important; border-bottom: 1px solid ${(props) => props.theme.border.border1}; padding: 6px 10px; h4 { font-size: ${(props) => props.theme.font.size.sm}; color: ${(props) => props.theme.text}; } label { font-size: ${(props) => props.theme.font.size.xs}; color: ${(props) => props.theme.colors.text.muted}; } } .copy-to-clipboard { button { background: ${(props) => props.theme.background.mantle}; border: 1px solid ${(props) => props.theme.border.border1}; border-radius: 3px; } } /* Dialog / modal overrides */ .dialog-ux { .modal-ux { background: ${(props) => props.theme.bg}; border: 1px solid ${(props) => props.theme.border.border1}; border-radius: 6px; color: ${(props) => props.theme.text}; box-shadow: 0 8px 32px rgba(0,0,0,0.4); .modal-ux-header { border-bottom: 1px solid ${(props) => props.theme.border.border1}; padding: 12px 0px; h3 { font-size: ${(props) => props.theme.font.size.md}; font-weight: 600; color: ${(props) => props.theme.text}; } .close-modal { opacity: 0.6; &:hover { opacity: 1; } svg { fill: ${(props) => props.theme.text}; } } } .modal-ux-content { color: ${(props) => props.theme.text}; padding: 12px 16px; p { font-size: ${(props) => props.theme.font.size.sm}; color: ${(props) => props.theme.colors.text.muted}; } /* Section headings like "api_key (apiKey)" */ h4, h5, h6 { font-size: ${(props) => props.theme.font.size.sm}; font-weight: 600; color: ${(props) => props.theme.textLink}; margin: 12px 0 6px; } /* Labels: "Name:", "In:", "Flow:", "Value:", etc. */ label { font-size: ${(props) => props.theme.font.size.sm}; color: ${(props) => props.theme.text}; > span { font-size: ${(props) => props.theme.font.size.sm}; color: ${(props) => props.theme.colors.text.muted}; } } /* "Scopes:" heading */ .scopes h2 { font-size: ${(props) => props.theme.font.size.sm} !important; font-weight: 500; color: ${(props) => props.theme.text} !important; } /* Scope item name + description */ .scopes .checkbox { p.name { font-size: ${(props) => props.theme.font.size.sm} !important; color: ${(props) => props.theme.text} !important; font-weight: 500; margin: 0; } p.description { font-size: ${(props) => props.theme.font.size.xs} !important; color: ${(props) => props.theme.colors.text.muted} !important; margin: 0; } } /* Text inputs */ input[type="text"], input[type="password"], input[type="email"] { background: ${(props) => props.theme.background.mantle} !important; color: ${(props) => props.theme.text} !important; border: 1px solid ${(props) => props.theme.border.border1} !important; border-radius: 4px !important; font-size: ${(props) => props.theme.font.size.sm} !important; padding: 6px 10px !important; outline: none !important; box-shadow: none !important; &:focus { border-color: ${(props) => props.theme.textLink} !important; outline: none !important; box-shadow: none !important; } } /* Checkboxes — custom styled to match theme */ input[type="checkbox"] { appearance: none !important; -webkit-appearance: none !important; width: 14px !important; height: 14px !important; min-width: 14px; border: 1px solid ${(props) => props.theme.border.border1} !important; border-radius: 3px !important; background: ${(props) => props.theme.background.mantle} !important; cursor: pointer; position: relative; vertical-align: middle; &:checked { background: ${(props) => props.theme.textLink} !important; border-color: ${(props) => props.theme.textLink} !important; &::after { content: ''; position: absolute; left: 3px; top: 1px; width: 5px; height: 8px; border: 2px solid #fff; border-top: none; border-left: none; transform: rotate(45deg); } } } /* "select all / select none" links */ a { font-size: ${(props) => props.theme.font.size.xs}; color: ${(props) => props.theme.textLink}; } /* Dividers between auth sections */ hr { border-color: ${(props) => props.theme.border.border1}; margin: 12px 0; } /* Authorize / Close buttons */ .btn-done, .auth-btn-wrapper .btn { font-size: ${(props) => props.theme.font.size.sm}; border-radius: 4px; padding: 6px 16px; border: 1px solid ${(props) => props.theme.border.border1}; background: transparent; color: ${(props) => props.theme.text}; cursor: pointer; outline: none !important; box-shadow: none !important; &:hover { background: ${(props) => props.theme.background.mantle}; } &.modal-btn-operation { background: ${(props) => props.theme.textLink}; color: #fff; border-color: transparent; &:hover { opacity: 0.9; } } } } } .backdrop-ux { background: rgba(0, 0, 0, 0.5); } } } } `; export default StyledWrapper; ================================================ FILE: packages/bruno-app/src/components/ApiSpecPanel/Renderers/Swagger/index.js ================================================ import SwaggerUI from 'swagger-ui-react'; import StyledWrapper from './StyledWrapper'; const Swagger = ({ spec }) => { return (
    ); }; export default Swagger; ================================================ FILE: packages/bruno-app/src/components/ApiSpecPanel/SpecViewer.js ================================================ import React, { useState, useEffect, Suspense } from 'react'; import get from 'lodash/get'; import { useTheme } from 'providers/Theme'; import { useSelector } from 'react-redux'; import { IconDeviceFloppy } from '@tabler/icons'; import CodeEditor from './FileEditor/CodeEditor/index'; import Swagger from './Renderers/Swagger'; /** * Shared split-pane spec viewer: CodeEditor (left) + Swagger preview (right). * * Props: * - content (string) The spec content (YAML/JSON string) * - readOnly (boolean) If true, editor is not editable and save icon is hidden * - onSave (function) Called with current editor content on save (editable mode only) */ const SpecViewer = ({ content, readOnly, onSave }) => { const { displayedTheme, theme } = useTheme(); const preferences = useSelector((state) => state.app.preferences); const [editorContent, setEditorContent] = useState(content); // Sync editor when saved content changes from outside (e.g. after save completes) useEffect(() => { setEditorContent(content); }, [content]); const hasChanges = !readOnly && editorContent !== content; const handleSave = () => { if (onSave) onSave(editorContent); }; return (
    setEditorContent(val)} onSave={readOnly ? undefined : handleSave} mode="yaml" font={get(preferences, 'font.codeFont', 'default')} /> {!readOnly && onSave && ( )}
    ); }; export default SpecViewer; ================================================ FILE: packages/bruno-app/src/components/ApiSpecPanel/StyledWrapper.js ================================================ import styled from 'styled-components'; const StyledWrapper = styled.div` .menu-icon { cursor: pointer; color: ${(props) => props.theme.sidebar.dropdownIcon.color}; } div.dropdown-item.menu-item { color: ${(props) => props.theme.colors.text.danger}; &:hover { background-color: ${(props) => props.theme.colors.bg.danger}; color: white; } } .react-tooltip { z-index: 10; } `; export default StyledWrapper; ================================================ FILE: packages/bruno-app/src/components/ApiSpecPanel/index.js ================================================ import React, { forwardRef, useRef } from 'react'; import find from 'lodash/find'; import { useSelector, useDispatch } from 'react-redux'; import { IconFileCode, IconDots } from '@tabler/icons'; import StyledWrapper from './StyledWrapper'; import SpecViewer from './SpecViewer'; import Dropdown from 'components/Dropdown'; import { openApiSpec, saveApiSpecToFile } from 'providers/ReduxStore/slices/apiSpec'; import { useState } from 'react'; import CreateApiSpec from 'components/Sidebar/ApiSpecs/CreateApiSpec'; import toast from 'react-hot-toast'; const ApiSpecPanel = () => { const dispatch = useDispatch(); const [createApiSpecModalOpen, setCreateApiSpecModalOpen] = useState(false); const { apiSpecs, activeApiSpecUid } = useSelector((state) => state.apiSpec); const dropdownTippyRef = useRef(); const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref); let apiSpec = find(apiSpecs, (c) => c.uid === activeApiSpecUid); const { filename, pathname, raw, uid } = apiSpec || {}; if (!uid) { return
    API Spec not found!
    ; } const MenuIcon = forwardRef((props, ref) => { return (
    ); }); const handleOpenApiSpec = () => { dispatch(openApiSpec()).catch( (err) => console.log(err) && toast.error('An error occurred while opening the API spec') ); }; return ( {createApiSpecModalOpen ? setCreateApiSpecModalOpen(false)} /> : null}
    API Designer
    {filename}
    } placement="bottom-start">
    { dropdownTippyRef.current.hide(); setCreateApiSpecModalOpen(true); }} > Create API Spec
    { dropdownTippyRef.current.hide(); handleOpenApiSpec(); }} > Open API Spec
    dispatch(saveApiSpecToFile({ uid, content }))} />
    ); }; export default ApiSpecPanel; ================================================ FILE: packages/bruno-app/src/components/AppTitleBar/AppMenu/StyledWrapper.js ================================================ import styled from 'styled-components'; const StyledWrapper = styled.div` display: flex; align-items: center; height: 100%; -webkit-app-region: no-drag; .shortcut { font-size: 11px; color: ${(props) => props.theme.dropdown.mutedText}; } `; export default StyledWrapper; ================================================ FILE: packages/bruno-app/src/components/AppTitleBar/AppMenu/index.js ================================================ import React, { useState } from 'react'; import { IconMenu2 } from '@tabler/icons'; import MenuDropdown from 'ui/MenuDropdown'; import ActionIcon from 'ui/ActionIcon'; import StyledWrapper from './StyledWrapper'; const AppMenu = () => { const [isOpen, setIsOpen] = useState(false); const { ipcRenderer } = window; const menuItems = [ { id: 'file', label: 'File', submenu: [ { id: 'open-collection', label: 'Open Collection', onClick: () => ipcRenderer?.invoke('renderer:open-collection') }, { type: 'divider', id: 'file-div-1' }, { id: 'preferences', label: 'Preferences', rightSection: Ctrl+,, onClick: () => ipcRenderer?.invoke('renderer:open-preferences') }, { type: 'divider', id: 'file-div-2' }, { id: 'quit', label: 'Quit', rightSection: Alt+F4, onClick: () => ipcRenderer?.send('renderer:window-close') } ] }, { id: 'edit', label: 'Edit', submenu: [ { id: 'undo', label: 'Undo', rightSection: Ctrl+Z, onClick: () => document.execCommand('undo') }, { id: 'redo', label: 'Redo', rightSection: Ctrl+Y, onClick: () => document.execCommand('redo') }, { type: 'divider', id: 'edit-div-1' }, { id: 'cut', label: 'Cut', rightSection: Ctrl+X, onClick: () => document.execCommand('cut') }, { id: 'copy', label: 'Copy', rightSection: Ctrl+C, onClick: () => document.execCommand('copy') }, { id: 'paste', label: 'Paste', rightSection: Ctrl+V, onClick: () => document.execCommand('paste') }, { type: 'divider', id: 'edit-div-2' }, { id: 'select-all', label: 'Select All', rightSection: Ctrl+A, onClick: () => document.execCommand('selectAll') } ] }, { id: 'view', label: 'View', submenu: [ { id: 'toggle-devtools', label: 'Developer Tools', rightSection: Ctrl+Shift+I, onClick: () => ipcRenderer?.invoke('renderer:toggle-devtools') }, { type: 'divider', id: 'view-div-1' }, { id: 'reset-zoom', label: 'Reset Zoom', rightSection: Ctrl+0, onClick: () => ipcRenderer?.invoke('renderer:reset-zoom') }, { id: 'zoom-in', label: 'Zoom In', rightSection: Ctrl++, onClick: () => ipcRenderer?.invoke('renderer:zoom-in') }, { id: 'zoom-out', label: 'Zoom Out', rightSection: Ctrl+-, onClick: () => ipcRenderer?.invoke('renderer:zoom-out') }, { type: 'divider', id: 'view-div-2' }, { id: 'toggle-fullscreen', label: 'Full Screen', rightSection: F11, onClick: () => ipcRenderer?.invoke('renderer:toggle-fullscreen') } ] }, { id: 'help', label: 'Help', submenu: [ { id: 'about', label: 'About Bruno', onClick: () => ipcRenderer?.invoke('renderer:open-about') }, { id: 'documentation', label: 'Documentation', onClick: () => ipcRenderer?.invoke('renderer:open-docs') } ] } ]; return ( ); }; export default AppMenu; ================================================ FILE: packages/bruno-app/src/components/AppTitleBar/StyledWrapper.js ================================================ import styled from 'styled-components'; const Wrapper = styled.div` height: 36px; display: flex; align-items: center; background: ${(props) => props.theme.sidebar.bg}; -webkit-app-region: drag; user-select: none; .titlebar-content { display: flex; align-items: center; justify-content: space-between; width: 100%; height: 100%; padding: 0 12px; padding-left: 70px; /* Space for macOS window controls */ transition: padding-left 0.15s ease; } /* When in full screen, no traffic lights so reduce padding */ &.fullscreen .titlebar-content { padding-left: 6px; } /* Remove drag region from interactive elements */ .workspace-name-container, .dropdown-item, .home-button, .dropdown, button { -webkit-app-region: no-drag; } /* Left section */ .titlebar-left { display: flex; align-items: center; flex-shrink: 0; margin-left: 10px; -webkit-app-region: no-drag; } /* When in full screen, no traffic lights so remove margin-left */ &.fullscreen .titlebar-left { margin-left: 0px; } /* Workspace Name Dropdown Trigger */ .workspace-name-container { display: flex; align-items: center; gap: 6px; padding: 5px 10px; border-radius: 6px; cursor: pointer; transition: all 0.15s ease; &:hover { background: ${(props) => props.theme.sidebar.collection.item.hoverBg}; } .workspace-name { font-size: 13px; font-weight: 500; color: ${(props) => props.theme.sidebar.color}; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 180px; } .chevron-icon { flex-shrink: 0; color: ${(props) => props.theme.sidebar.muted}; transition: transform 0.2s ease; } } /* Center section - Bruno branding */ .titlebar-center { position: absolute; left: 50%; transform: translateX(-50%); display: flex; align-items: center; gap: 6px; pointer-events: none; .bruno-text { font-size: 13px; font-weight: 600; color: ${(props) => props.theme.text}; letter-spacing: 0.5px; } } /* Right section */ .titlebar-right { display: flex; align-items: center; justify-content: flex-end; flex-shrink: 0; -webkit-app-region: no-drag; } /* App action buttons container */ .titlebar-actions { display: flex; align-items: center; } /* Workspace Dropdown Styles */ .workspace-item { display: flex; align-items: center; justify-content: space-between; padding: 4px 10px !important; margin: 0 !important; &.active { .check-icon { opacity: 1; } } &:hover { .pin-btn:not(.pinned) { opacity: 1; } } .workspace-name { flex: 1; min-width: 0; font-size: 13px; font-weight: 400; color: ${(props) => props.theme.dropdown.color}; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .workspace-actions { display: flex; align-items: center; gap: 4px; margin-left: 8px; flex-shrink: 0; pointer-events: none; > * { pointer-events: auto; } } .check-icon { color: ${(props) => props.theme.workspace?.accent || props.theme.colors?.text?.yellow}; flex-shrink: 0; } .pin-btn { display: flex; align-items: center; justify-content: center; width: 22px; height: 22px; padding: 0; border: none; background: transparent; border-radius: 4px; cursor: pointer; color: ${(props) => props.theme.dropdown.mutedText}; transition: background 0.15s ease, color 0.15s ease, opacity 0.15s ease; opacity: 0; &.pinned { opacity: 1; } &:hover { background: ${(props) => props.theme.dropdown.hoverBg}; color: ${(props) => props.theme.dropdown.mutedText}; } } } /* Adjust for non-macOS platforms */ &:not(.os-mac) .titlebar-content { padding-left: 12px; } /* Windows-specific styles */ &.os-windows .titlebar-content { padding-right: 0px; padding-left: 0px; } &.os-windows .titlebar-left { margin-left: 6px; } &.os-linux .titlebar-content { padding-right: 0px; padding-left: 0px; } &.os-linux .titlebar-left { margin-left: 6px; } .app-menu { margin-left: 8px; } /* Custom window control buttons for Windows - always interactive, above modal overlay */ .window-controls { display: flex; align-items: stretch; height: 36px; margin-left: 8px; position: relative; z-index: 1000; } .window-control-btn { display: flex; align-items: center; justify-content: center; width: 46px; height: 100%; border: none; background: transparent; color: ${(props) => props.theme.text}; cursor: pointer; transition: background-color 0.1s ease; -webkit-app-region: no-drag; &:hover { background: ${(props) => props.theme.sidebar.collection.item.hoverBg}; } &:active { background: ${(props) => props.theme.sidebar.collection.item.hoverBg}; } &.close:hover { background: #e81123; color: white; } } `; export default Wrapper; ================================================ FILE: packages/bruno-app/src/components/AppTitleBar/index.js ================================================ import React from 'react'; import { IconCheck, IconChevronDown, IconFolder, IconHome, IconPin, IconPinned, IconPlus, IconDownload, IconSettings, IconMinus, IconSquare, IconX, IconCopy } from '@tabler/icons'; import { forwardRef, useCallback, useEffect, useMemo, useState } from 'react'; import toast from 'react-hot-toast'; import { useDispatch, useSelector } from 'react-redux'; import { savePreferences, showManageWorkspacePage, toggleSidebarCollapse } from 'providers/ReduxStore/slices/app'; import { closeConsole, openConsole } from 'providers/ReduxStore/slices/logs'; import { createWorkspaceWithUniqueName, openWorkspaceDialog, switchWorkspace } from 'providers/ReduxStore/slices/workspaces/actions'; import { sortWorkspaces, toggleWorkspacePin } from 'utils/workspaces'; import { focusTab } from 'providers/ReduxStore/slices/tabs'; import get from 'lodash/get'; import Bruno from 'components/Bruno'; import MenuDropdown from 'ui/MenuDropdown'; import ActionIcon from 'ui/ActionIcon'; import IconSidebarToggle from 'components/Icons/IconSidebarToggle'; import CreateWorkspace from 'components/WorkspaceSidebar/CreateWorkspace'; import ImportWorkspace from 'components/WorkspaceSidebar/ImportWorkspace'; import IconBottombarToggle from 'components/Icons/IconBottombarToggle/index'; import AppMenu from './AppMenu'; import StyledWrapper from './StyledWrapper'; import ResponseLayoutToggle from 'components/ResponsePane/ResponseLayoutToggle'; import { isMacOS, isWindowsOS, isLinuxOS } from 'utils/common/platform'; import classNames from 'classnames'; const getOsClass = () => { if (isMacOS()) return 'os-mac'; if (isWindowsOS()) return 'os-windows'; if (isLinuxOS()) return 'os-linux'; return 'os-other'; }; // Helper to get display name for workspace export const getWorkspaceDisplayName = (name) => { if (!name) return 'Untitled Workspace'; return name; }; const AppTitleBar = () => { const dispatch = useDispatch(); const [isFullScreen, setIsFullScreen] = useState(false); const [isMaximized, setIsMaximized] = useState(false); const osClass = getOsClass(); const isWindows = osClass === 'os-windows'; const isLinux = osClass === 'os-linux'; const showWindowControls = isWindows || isLinux; // Listen for fullscreen changes useEffect(() => { const { ipcRenderer } = window; if (!ipcRenderer) return; const removeEnterFullScreenListener = ipcRenderer.on('main:enter-full-screen', () => { setIsFullScreen(true); }); const removeLeaveFullScreenListener = ipcRenderer.on('main:leave-full-screen', () => { setIsFullScreen(false); }); return () => { removeEnterFullScreenListener(); removeLeaveFullScreenListener(); }; }, []); useEffect(() => { if (!showWindowControls) return; const { ipcRenderer } = window; if (!ipcRenderer) return; ipcRenderer.invoke('renderer:window-is-maximized') .then((maximized) => { setIsMaximized(maximized); }) .catch((error) => { console.error('Error getting initial maximized state:', error); }); const removeMaximizedListener = ipcRenderer.on('main:window-maximized', () => { setIsMaximized(true); }); const removeUnmaximizedListener = ipcRenderer.on('main:window-unmaximized', () => { setIsMaximized(false); }); return () => { removeMaximizedListener(); removeUnmaximizedListener(); }; }, [showWindowControls]); const handleMinimize = useCallback(() => { window.ipcRenderer?.send('renderer:window-minimize'); }, []); const handleMaximize = useCallback(() => { window.ipcRenderer?.send('renderer:window-maximize'); // State will be updated via IPC events from main process (main:window-maximized/main:window-unmaximized) }, []); const handleClose = useCallback(() => { window.ipcRenderer?.send('renderer:window-close'); }, []); // Get workspace info const { workspaces, activeWorkspaceUid } = useSelector((state) => state.workspaces); const preferences = useSelector((state) => state.app.preferences); const sidebarCollapsed = useSelector((state) => state.app.sidebarCollapsed); const isConsoleOpen = useSelector((state) => state.logs.isConsoleOpen); const activeWorkspace = workspaces.find((w) => w.uid === activeWorkspaceUid); // Sort workspaces according to preferences const sortedWorkspaces = useMemo(() => { return sortWorkspaces(workspaces, preferences); }, [workspaces, preferences]); const [createWorkspaceModalOpen, setCreateWorkspaceModalOpen] = useState(false); const [importWorkspaceModalOpen, setImportWorkspaceModalOpen] = useState(false); const WorkspaceName = forwardRef((props, ref) => { return (
    {getWorkspaceDisplayName(activeWorkspace?.name)}
    ); }); const handleHomeClick = () => { const scratchCollectionUid = activeWorkspace?.scratchCollectionUid; if (scratchCollectionUid) { dispatch(focusTab({ uid: `${scratchCollectionUid}-overview` })); } }; const handleWorkspaceSwitch = (workspaceUid) => { dispatch(switchWorkspace(workspaceUid)); toast.success(`Switched to ${getWorkspaceDisplayName(workspaces.find((w) => w.uid === workspaceUid)?.name)}`); }; const handleOpenWorkspace = async () => { try { await dispatch(openWorkspaceDialog()); toast.success('Workspace opened successfully'); } catch (error) { toast.error(error.message || 'Failed to open workspace'); } }; const handleCreateWorkspace = useCallback(async () => { const defaultLocation = get(preferences, 'general.defaultLocation', ''); if (!defaultLocation) { setCreateWorkspaceModalOpen(true); return; } try { await dispatch(createWorkspaceWithUniqueName(defaultLocation)); } catch (error) { toast.error(error?.message || 'Failed to create workspace'); } }, [preferences, dispatch]); const handleManageWorkspaces = () => { dispatch(showManageWorkspacePage()); }; const handleImportWorkspace = () => { setImportWorkspaceModalOpen(true); }; const handlePinWorkspace = useCallback((workspaceUid, e) => { e.preventDefault(); e.stopPropagation(); const newPreferences = toggleWorkspacePin(workspaceUid, preferences); dispatch(savePreferences(newPreferences)); }, [dispatch, preferences]); const handleToggleSidebar = () => { dispatch(toggleSidebarCollapse()); }; const handleToggleDevtools = () => { if (isConsoleOpen) { dispatch(closeConsole()); } else { dispatch(openConsole()); } }; // Build workspace menu items const workspaceMenuItems = useMemo(() => { const items = sortedWorkspaces.map((workspace) => { const isActive = workspace.uid === activeWorkspaceUid; const isPinned = preferences?.workspaces?.pinnedWorkspaceUids?.includes(workspace.uid); return { id: workspace.uid, label: getWorkspaceDisplayName(workspace.name), onClick: () => handleWorkspaceSwitch(workspace.uid), className: `workspace-item ${isActive ? 'active' : ''}`, rightSection: (
    {workspace.type !== 'default' && ( handlePinWorkspace(workspace.uid, e)} label={isPinned ? 'Unpin workspace' : 'Pin workspace'} size="sm" > {isPinned ? : } )} {isActive && }
    ) }; }); // Add label and action items items.push( { type: 'label', label: 'Workspaces' }, { id: 'create-workspace', leftSection: IconPlus, label: 'Create workspace', onClick: handleCreateWorkspace }, { id: 'open-workspace', leftSection: IconFolder, label: 'Open workspace', onClick: handleOpenWorkspace }, { id: 'import-workspace', leftSection: IconDownload, label: 'Import workspace', onClick: handleImportWorkspace }, { id: 'manage-workspaces', leftSection: IconSettings, label: 'Manage workspaces', onClick: handleManageWorkspaces } ); return items; }, [sortedWorkspaces, activeWorkspaceUid, preferences, handlePinWorkspace, handleCreateWorkspace]); return ( {createWorkspaceModalOpen && ( setCreateWorkspaceModalOpen(false)} /> )} {importWorkspaceModalOpen && ( setImportWorkspaceModalOpen(false)} /> )}
    {showWindowControls && } {/* Workspace Dropdown */}
    {/* Center section: Bruno logo + text */}
    Bruno
    {/* Right section: Action buttons */}
    {/* Toggle sidebar */} {/* Toggle devtools */}
    {showWindowControls && (
    )}
    ); }; export default AppTitleBar; ================================================ FILE: packages/bruno-app/src/components/BodyModeSelector/StyledWrapper.js ================================================ import styled from 'styled-components'; const StyledWrapper = styled.div` font-size: ${(props) => props.theme.font.size.base}; .body-mode-selector { background: transparent; border-radius: 3px; .dropdown-item { padding: 0.2rem 0.6rem !important; padding-left: 1.5rem !important; display: flex; align-items: center; } .label-item { padding: 0.2rem 0.6rem !important; } .selected-body-mode { color: ${(props) => props.theme.primary.text}; } .dropdown-icon { display: flex; align-items: center; margin-right: 0.5rem; } } .caret { color: ${(props) => props.theme.colors.text.muted}; fill: ${(props) => props.theme.colors.text.muted}; } `; export default StyledWrapper; ================================================ FILE: packages/bruno-app/src/components/BodyModeSelector/index.js ================================================ import React, { useMemo } from 'react'; import { IconCaretDown, IconForms, IconBraces, IconCode, IconFileText, IconDatabase, IconFile, IconX } from '@tabler/icons'; import MenuDropdown from 'ui/MenuDropdown'; import { humanizeRequestBodyMode } from 'utils/collections'; import StyledWrapper from './StyledWrapper'; const DEFAULT_MODES = [ { name: 'Form', options: [ { id: 'multipartForm', label: 'Multipart Form', leftSection: IconForms }, { id: 'formUrlEncoded', label: 'Form URL Encoded', leftSection: IconForms } ] }, { name: 'Raw', options: [ { id: 'json', label: 'JSON', leftSection: IconBraces }, { id: 'xml', label: 'XML', leftSection: IconCode }, { id: 'text', label: 'TEXT', leftSection: IconFileText }, { id: 'sparql', label: 'SPARQL', leftSection: IconDatabase } ] }, { name: 'Other', options: [ { id: 'file', label: 'File / Binary', leftSection: IconFile }, { id: 'none', label: 'No Body', leftSection: IconX } ] } ]; const BodyModeSelector = ({ currentMode, onModeChange, modes = DEFAULT_MODES, disabled = false, className = '', wrapperClassName = '', placement = 'bottom-end' }) => { // Add onClick handlers to mode options const menuItems = useMemo(() => { return modes.map((group) => ({ ...group, options: group.options.map((option) => ({ ...option, onClick: () => onModeChange(option.id) })) })); }, [modes, onModeChange]); return (
    {humanizeRequestBodyMode(currentMode)} {' '}
    ); }; export default BodyModeSelector; ================================================ FILE: packages/bruno-app/src/components/Bruno/index.js ================================================ import React from 'react'; const Bruno = ({ width }) => { return ( ); }; export default Bruno; ================================================ FILE: packages/bruno-app/src/components/BrunoSupport/StyledWrapper.js ================================================ import styled from 'styled-components'; const StyledWrapper = styled.div` color: ${(props) => props.theme.text}; .collection-options { svg { position: relative; top: -1px; } .label { cursor: pointer; &:hover { text-decoration: underline; } } } `; export default StyledWrapper; ================================================ FILE: packages/bruno-app/src/components/BrunoSupport/index.js ================================================ import React from 'react'; import Modal from 'components/Modal/index'; import { IconSpeakerphone, IconBrandTwitter, IconBrandGithub, IconBrandDiscord, IconBook } from '@tabler/icons'; import StyledWrapper from './StyledWrapper'; const BrunoSupport = ({ onClose }) => { return ( ); }; export default BrunoSupport; ================================================ FILE: packages/bruno-app/src/components/BulkEditor/index.js ================================================ import React, { useMemo } from 'react'; import get from 'lodash/get'; import CodeEditor from 'components/CodeEditor'; import { useTheme } from 'providers/Theme'; import { useSelector } from 'react-redux'; import { parseBulkKeyValue, serializeBulkKeyValue } from 'utils/common/bulkKeyValueUtils'; const BulkEditor = ({ params, onChange, onToggle, onSave, onRun }) => { const preferences = useSelector((state) => state.app.preferences); const { displayedTheme } = useTheme(); const parsedParams = useMemo(() => serializeBulkKeyValue(params), [params]); const handleEdit = (value) => { const parsed = parseBulkKeyValue(value); onChange(parsed); }; return ( <>
    ); }; export default BulkEditor; ================================================ FILE: packages/bruno-app/src/components/Checkbox/StyledWrapper.js ================================================ import styled from 'styled-components'; const StyledWrapper = styled.div` .checkbox-container { width: 1rem; height: 1rem; display: flex; justify-content: center; align-items: center; position: relative; cursor: pointer; &:disabled { cursor: not-allowed; opacity: 0.5; } } .checkbox-checkmark { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); visibility: ${(props) => props.checked ? 'visible' : 'hidden'}; pointer-events: none; } .checkbox-input { appearance: none; -webkit-appearance: none; -moz-appearance: none; width: 1rem; height: 1rem; border: 2px solid ${(props) => { if (props.checked && props.disabled) { return props.theme.colors.text.muted; } if (props.checked && !props.disabled) { return props.theme.colors.text.yellow; } return props.theme.colors.text.muted; }}; border-radius: 4px; background-color: ${(props) => { if (props.checked && !props.disabled) { return props.theme.colors.text.yellow; } if (props.checked && props.disabled) { return props.theme.colors.text.muted; } return 'transparent'; }}; cursor: pointer; position: relative; transition: all 0.2s ease; outline: none; box-shadow: none; &:hover:not(:disabled) { opacity: 0.8; } &:disabled { cursor: not-allowed; opacity: 0.5; } &:focus { outline: none; box-shadow: 0 0 0 2px ${(props) => props.theme.colors.text.yellow}40; } } `; export default StyledWrapper; ================================================ FILE: packages/bruno-app/src/components/Checkbox/index.js ================================================ import React from 'react'; import StyledWrapper from './StyledWrapper'; import IconCheckMark from 'components/Icons/IconCheckMark'; import { useTheme } from 'providers/Theme'; const Checkbox = ({ checked = false, disabled = false, onChange, className = '', id, name, value, dataTestId = 'checkbox' }) => { const { theme } = useTheme(); const handleChange = (e) => { if (!disabled && onChange) { onChange(e); } }; return (
    ); }; export default Checkbox; ================================================ FILE: packages/bruno-app/src/components/CodeEditor/StyledWrapper.js ================================================ import styled from 'styled-components'; const StyledWrapper = styled.div` &.read-only { div.CodeMirror .CodeMirror-cursor { display: none !important; } } div.CodeMirror { background: ${(props) => props.theme.codemirror.bg}; border: solid 1px ${(props) => props.theme.codemirror.border}; font-family: ${(props) => (props.font ? props.font : 'default')}; font-size: ${(props) => (props.fontSize ? `${props.fontSize}px` : 'inherit')}; line-break: anywhere; flex: 1 1 0; display: flex; flex-direction: column-reverse; } .CodeMirror-placeholder { color: ${(props) => props.theme.text} !important; opacity: 0.5 !important; } .CodeMirror-linenumber { text-align: left !important; padding-left: 3px !important; } /* Override default lint highlight background when emphasizing the gutter */ .CodeMirror-lint-line-error, .CodeMirror-lint-line-warning { background: none !important; } /* Style line numbers when there's a lint issue */ .CodeMirror-lint-line-error .CodeMirror-linenumber { color: ${(props) => props.theme.colors.text.danger} !important; text-decoration: underline; } .CodeMirror-lint-line-warning .CodeMirror-linenumber { color: ${(props) => props.theme.colors.text.warning} !important; text-decoration: underline; } /* Removes the glow outline around the folded json */ .CodeMirror-foldmarker { text-shadow: none; color: ${(props) => props.theme.textLink}; background: none; padding: 0; margin: 0; } .CodeMirror-overlayscroll-horizontal div, .CodeMirror-overlayscroll-vertical div { background: #d2d7db; } .CodeMirror-dialog { overflow: visible; position: relative; top: unset; left: unset; input { background: transparent; border: 1px solid #d3d6db; outline: none; border-radius: 0px; } } #search-results-count { display: inline-block; position: absolute; top: calc(100% + 1px); right: 0; border-width: 0 0 1px 1px; border-style: solid; border-color: ${(props) => props.theme.codemirror.border}; padding: 0.1em 0.8em; background-color: ${(props) => props.theme.codemirror.bg}; color: rgb(102, 102, 102); white-space: nowrap; } textarea.cm-editor { position: relative; } // Todo: dark mode temporary fix // Clean this .CodeMirror.cm-s-monokai { .CodeMirror-overlayscroll-horizontal div, .CodeMirror-overlayscroll-vertical div { background: #444444; } } .cm-s-default, .cm-s-monokai { span.cm-def { color: ${(props) => props.theme.codemirror.tokens.definition} !important; } span.cm-property { color: ${(props) => props.theme.codemirror.tokens.property} !important; } span.cm-string { color: ${(props) => props.theme.codemirror.tokens.string} !important; } span.cm-number { color: ${(props) => props.theme.codemirror.tokens.number} !important; } span.cm-atom { color: ${(props) => props.theme.codemirror.tokens.atom} !important; } span.cm-variable, span.cm-variable-2 { color: ${(props) => props.theme.codemirror.tokens.variable} !important; } span.cm-keyword { color: ${(props) => props.theme.codemirror.tokens.keyword} !important; } span.cm-comment { color: ${(props) => props.theme.codemirror.tokens.comment} !important; } span.cm-operator { color: ${(props) => props.theme.codemirror.tokens.operator} !important; } span.cm-tag { color: ${(props) => props.theme.codemirror.tokens.tag} !important; } span.cm-tag.cm-bracket { color: ${(props) => props.theme.codemirror.tokens.tagBracket} !important; } } /* Variable validation colors */ .cm-variable-valid { color: ${(props) => props.theme.codemirror.variable.valid} !important; } .cm-variable-invalid { color: ${(props) => props.theme.codemirror.variable.invalid} !important; } .CodeMirror-search-hint { display: inline; } //matching bracket fix .CodeMirror-matchingbracket { background: #5cc0b48c !important; text-decoration:unset; } .cm-search-line-highlight { background: ${(props) => props.theme.codemirror.searchLineHighlightCurrent}; } .cm-search-match { background: rgba(255, 193, 7, 0.25); } .cm-search-current { background: rgba(255, 193, 7, 0.4); } .lint-error-tooltip { position: fixed; z-index: 10000; background: ${(props) => props.theme.codemirror.bg}; border-radius: ${(props) => props.theme.border.radius.base}; padding: 8px 12px; max-width: 400px; box-shadow: ${(props) => props.theme.shadow.sm}; font-size: ${(props) => props.theme.font.size.xs}; line-height: 1.5; pointer-events: none; .lint-tooltip-message { padding: 2px 0; } .lint-tooltip-message.error { color: ${(props) => props.theme.colors.text.danger}; } .lint-tooltip-message.warning { color: ${(props) => props.theme.colors.text.warning}; } } `; export default StyledWrapper; ================================================ FILE: packages/bruno-app/src/components/CodeEditor/index.js ================================================ /** * Copyright (c) 2021 GraphQL Contributors. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ import React, { createRef } from 'react'; import { isEqual, escapeRegExp } from 'lodash'; import { defineCodeMirrorBrunoVariablesMode } from 'utils/common/codemirror'; import { setupAutoComplete, showRootHints } from 'utils/codemirror/autocomplete'; import StyledWrapper from './StyledWrapper'; import * as jsonlint from '@prantlf/jsonlint'; import { JSHINT } from 'jshint'; import stripJsonComments from 'strip-json-comments'; import { getAllVariables } from 'utils/collections'; import { setupLinkAware } from 'utils/codemirror/linkAware'; import { setupLintErrorTooltip } from 'utils/codemirror/lint-errors'; import CodeMirrorSearch from 'components/CodeMirrorSearch/index'; const CodeMirror = require('codemirror'); window.jsonlint = jsonlint; window.JSHINT = JSHINT; const TAB_SIZE = 2; export default class CodeEditor extends React.Component { constructor(props) { super(props); // Keep a cached version of the value, this cache will be updated when the // editor is updated, which can later be used to protect the editor from // unnecessary updates during the update lifecycle. this.cachedValue = props.value || ''; this.variables = {}; this.searchResultsCountElementId = 'search-results-count'; this.searchBarRef = createRef(); this.lintOptions = { esversion: 11, expr: true, asi: true, highlightLines: true }; this.state = { searchBarVisible: false }; } componentDidMount() { const variables = getAllVariables(this.props.collection, this.props.item); const editor = (this.editor = CodeMirror(this._node, { value: this.props.value || '', placeholder: '...', lineNumbers: true, lineWrapping: this.props.enableLineWrapping ?? true, tabSize: TAB_SIZE, mode: this.props.mode || 'application/ld+json', brunoVarInfo: this.props.enableBrunoVarInfo !== false ? { variables, collection: this.props.collection, item: this.props.item } : false, keyMap: 'sublime', autoCloseBrackets: true, matchBrackets: true, showCursorWhenSelecting: true, foldGutter: true, gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'], lint: this.lintOptions, readOnly: this.props.readOnly, scrollbarStyle: 'overlay', theme: this.props.theme === 'dark' ? 'monokai' : 'default', extraKeys: { 'Cmd-Enter': () => { if (this.props.onRun) { this.props.onRun(); } }, 'Ctrl-Enter': () => { if (this.props.onRun) { this.props.onRun(); } }, 'Cmd-S': () => { if (this.props.onSave) { this.props.onSave(); } }, 'Ctrl-S': () => { if (this.props.onSave) { this.props.onSave(); } }, 'Cmd-F': (cm) => { this.setState({ searchBarVisible: true }, () => { this.searchBarRef.current?.focus(); }); }, 'Ctrl-F': (cm) => { this.setState({ searchBarVisible: true }, () => { this.searchBarRef.current?.focus(); }); }, 'Cmd-H': 'replace', 'Ctrl-H': 'replace', 'Tab': function (cm) { cm.getSelection().includes('\n') || editor.getLine(cm.getCursor().line) == cm.getSelection() ? cm.execCommand('indentMore') : cm.replaceSelection(' ', 'end'); }, 'Shift-Tab': 'indentLess', 'Ctrl-Space': (cm) => { showRootHints(cm, this.props.showHintsFor); }, 'Cmd-Space': (cm) => { showRootHints(cm, this.props.showHintsFor); }, 'Ctrl-Y': 'foldAll', 'Cmd-Y': 'foldAll', 'Ctrl-I': 'unfoldAll', 'Cmd-I': 'unfoldAll', 'Ctrl-/': () => { if (['application/ld+json', 'application/json'].includes(this.props.mode)) { this.editor.toggleComment({ lineComment: '//', blockComment: '/*' }); } else { this.editor.toggleComment(); } }, 'Cmd-/': () => { if (['application/ld+json', 'application/json'].includes(this.props.mode)) { this.editor.toggleComment({ lineComment: '//', blockComment: '/*' }); } else { this.editor.toggleComment(); } }, 'Esc': () => { if (this.state.searchBarVisible) { this.setState({ searchBarVisible: false }); } } }, foldOptions: { widget: (from, to) => { var count = undefined; var internal = this.editor.getRange(from, to); if (this.props.mode == 'application/ld+json') { if (this.editor.getLine(from.line).endsWith('[')) { var toParse = '[' + internal + ']'; } else var toParse = '{' + internal + '}'; try { count = Object.keys(JSON.parse(toParse)).length; } catch (e) {} } else if (this.props.mode == 'application/xml') { var doc = new DOMParser(); try { // add header element and remove prefix namespaces for DOMParser var dcm = doc.parseFromString( ' ' + internal.replace(/(?<=\<|<\/)\w+:/g, '') + '', 'application/xml' ); count = dcm.documentElement.children.length; } catch (e) {} } return count ? `\u21A4${count}\u21A6` : '\u2194'; } } })); CodeMirror.registerHelper('lint', 'json', function (text) { let found = []; if (!window.jsonlint) { if (window.console) { window.console.error('Error: window.jsonlint not defined, CodeMirror JSON linting cannot run.'); } return found; } let jsonlint = window.jsonlint.parser || window.jsonlint; try { jsonlint.parse(stripJsonComments(text.replace(/(? 0 ? this.lintOptions : false); editor.on('change', this._onEdit); editor.scrollTo(null, this.props.initialScroll); this.addOverlay(); const getAllVariablesHandler = () => getAllVariables(this.props.collection, this.props.item); // Setup AutoComplete Helper for all modes const autoCompleteOptions = { showHintsFor: this.props.showHintsFor, getAllVariables: getAllVariablesHandler }; this.brunoAutoCompleteCleanup = setupAutoComplete( editor, autoCompleteOptions ); setupLinkAware(editor); // Setup lint error tooltip on line number hover this.cleanupLintErrorTooltip = setupLintErrorTooltip(editor); } } componentDidUpdate(prevProps) { // Ensure the changes caused by this update are not interpreted as // user-input changes which could otherwise result in an infinite // event loop. this.ignoreChangeEvent = true; if (this.props.schema !== prevProps.schema && this.editor) { this.editor.options.lint.schema = this.props.schema; this.editor.options.hintOptions.schema = this.props.schema; this.editor.options.info.schema = this.props.schema; this.editor.options.jump.schema = this.props.schema; CodeMirror.signal(this.editor, 'change', this.editor); } if (this.props.value !== prevProps.value && this.props.value !== this.cachedValue && this.editor) { // TODO: temporary fix for keeping cursor state when auto save and new line insertion collide PR#7098 const nextValue = this.props.value ?? ''; const currentValue = this.editor.getValue(); // Skip updating only when focused and editable; read-only editors (e.g. response viewer) must always show new value if (this.editor.hasFocus?.() && currentValue !== nextValue && !this.props.readOnly) { this.cachedValue = currentValue; } else { const cursor = this.editor.getCursor(); this.cachedValue = nextValue; this.editor.setValue(nextValue); this.editor.setCursor(cursor); } } if (this.editor) { let variables = getAllVariables(this.props.collection, this.props.item); if (!isEqual(variables, this.variables)) { this.addOverlay(); } // Update collection and item when they change if (this.props.enableBrunoVarInfo !== false && this.editor.options.brunoVarInfo) { if (!isEqual(this.props.collection, this.editor.options.brunoVarInfo.collection)) { this.editor.options.brunoVarInfo.collection = this.props.collection; } if (!isEqual(this.props.item, this.editor.options.brunoVarInfo.item)) { this.editor.options.brunoVarInfo.item = this.props.item; } } } if (this.props.theme !== prevProps.theme && this.editor) { this.editor.setOption('theme', this.props.theme === 'dark' ? 'monokai' : 'default'); } if (this.props.initialScroll !== prevProps.initialScroll) { this.editor.scrollTo(null, this.props.initialScroll); } if (this.props.enableLineWrapping !== prevProps.enableLineWrapping) { this.editor.setOption('lineWrapping', this.props.enableLineWrapping); } if (this.props.mode !== prevProps.mode) { this.editor.setOption('mode', this.props.mode); } if (this.props.readOnly !== prevProps.readOnly && this.editor) { this.editor.setOption('readOnly', this.props.readOnly); } this.ignoreChangeEvent = false; } componentWillUnmount() { if (this.editor) { if (this.props.onScroll) { this.props.onScroll(this.editor); } this.editor?._destroyLinkAware?.(); this.editor.off('change', this._onEdit); // Clean up lint error tooltip this.cleanupLintErrorTooltip?.(); const wrapper = this.editor.getWrapperElement(); wrapper?.parentNode?.removeChild(wrapper); this.editor = null; } } render() { if (this.editor) { this.editor.refresh(); } return ( { if (!node) return; this.searchBarRef.current = node; }} visible={this.state.searchBarVisible} editor={this.editor} onClose={() => this.setState({ searchBarVisible: false })} />
    { this._node = node; }} style={{ height: '100%', width: '100%' }} /> ); } addOverlay = () => { const mode = this.props.mode || 'application/ld+json'; let variables = getAllVariables(this.props.collection, this.props.item); this.variables = variables; // Update brunoVarInfo with latest variables if (this.props.enableBrunoVarInfo !== false && this.editor.options.brunoVarInfo) { this.editor.options.brunoVarInfo.variables = variables; } defineCodeMirrorBrunoVariablesMode(variables, mode, false, this.props.enableVariableHighlighting); this.editor.setOption('mode', 'brunovariables'); }; _onEdit = () => { if (!this.ignoreChangeEvent && this.editor) { this.editor.setOption('lint', this.editor.getValue().trim().length > 0 ? this.lintOptions : false); this.cachedValue = this.editor.getValue(); if (this.props.onEdit) { this.props.onEdit(this.cachedValue); } } }; } ================================================ FILE: packages/bruno-app/src/components/CodeEditor/index.spec.js ================================================ import React from 'react'; import { render, act } from '@testing-library/react'; import CodeEditor from './index'; import { ThemeProvider } from 'styled-components'; jest.mock('codemirror', () => { const codemirror = require('test-utils/mocks/codemirror'); return codemirror; }); const MOCK_THEME = { codemirror: { bg: '#1e1e1e', border: '#333' }, textLink: '#007acc' }; const setupEditorState = (editor, { value, cursorPosition }) => { editor._currentValue = value; editor.getCursor.mockReturnValue({ line: 0, ch: cursorPosition }); editor.getRange.mockImplementation((from, to) => { if (from.line === 0 && from.ch === 0 && to.line === 0 && to.ch === cursorPosition) { return value; } return editor._currentValue.slice(from.ch, to.ch); }); editor.state = { completionActive: null }; }; const setupEditorWithRef = () => { const ref = React.createRef(); const { rerender } = render( ); return { ref, rerender }; }; describe('CodeEditor', () => { beforeEach(() => { jest.clearAllMocks(); jest.resetModules(); }); it('add CodeEditor related tests here', () => {}); }); ================================================ FILE: packages/bruno-app/src/components/CodeMirrorSearch/StyledWrapper.js ================================================ import styled from 'styled-components'; import { rgba } from 'polished'; const StyledWrapper = styled.div` .bruno-search-bar { position: absolute; top: 8px; right: 8px; z-index: 20; display: flex; align-items: center; flex-wrap: nowrap; gap: 0; padding: 1px 3px; width: auto; max-width: 320px; min-height: 22px; background: ${(props) => props.theme.background.base}; color: ${(props) => props.theme.text.base}; border: solid 1px ${(props) => props.theme.border.border2}; border-radius: ${(props) => props.theme.border.radius.sm}; } .bruno-search-bar input { min-width: 80px; background: transparent; color: inherit; border: none; outline: none; padding: 1px 2px; font-size: ${(props) => props.theme.font.size.base}; margin: 0 1px; height: 28px; } .searchbar-icon-btn { background: none; border: none; padding: 0 1px; margin: 0 1px; cursor: pointer; color: ${(props) => props.theme.colors.text.subtext1}; border-radius: 3px; height: 18px; width: 18px; display: flex; align-items: center; justify-content: center; } .searchbar-result-count { min-width: 28px; text-align: center; font-size: ${(props) => props.theme.font.size.xs}; color: ${(props) => props.theme.colors.text.subtext1}; margin: 0 8px 0 1px; white-space: nowrap; } .bruno-search-bar input { background: transparent; color: ${(props) => props.theme.colors.text.subtext2}; border: none; outline: none; font-size: ${(props) => props.theme.font.size.base}; padding: 1px 2px; min-width: 80px; } .searchbar-icon-btn:focus { outline: 1px solid ${(props) => props.theme.codemirror.border}; } .bruno-search-bar, .bruno-search-bar input { font-family: Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif !important; } .cm-search-line-highlight { background: ${(props) => props.theme.codemirror.searchLineHighlightCurrent}; } .searchbar-icon-btn.active { color: ${(props) => props.theme.brand}; background-color: ${(props) => rgba(props.theme.brand, 0.1)}; font-weight: 500; } `; export default StyledWrapper; ================================================ FILE: packages/bruno-app/src/components/CodeMirrorSearch/index.js ================================================ import React, { useState, useEffect, useRef, useCallback, useMemo, forwardRef, useImperativeHandle } from 'react'; import { IconRegex, IconArrowUp, IconArrowDown, IconX, IconLetterCase, IconLetterW } from '@tabler/icons'; import ToolHint from 'components/ToolHint'; import StyledWrapper from './StyledWrapper'; import useDebounce from 'hooks/useDebounce'; function escapeRegExp(string) { return string.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&'); } const MAX_MATCHES = 99_999; function findSearchMatches(editor, searchText, regex, caseSensitive, wholeWord) { try { let query, options = {}; if (regex) { try { query = new RegExp(searchText, caseSensitive ? 'g' : 'gi'); } catch (error) { console.warn('Invalid regex provided in search!', error); return []; } } else if (wholeWord) { const escaped = escapeRegExp(searchText); query = new RegExp(`\\b${escaped}\\b`, caseSensitive ? 'g' : 'gi'); } else { query = searchText; options = { caseFold: !caseSensitive }; } const cursor = editor.getSearchCursor(query, { line: 0, ch: 0 }, options); const out = []; while (cursor.findNext()) { out.push({ from: cursor.from(), to: cursor.to() }); if (out.length >= MAX_MATCHES) { break; } } return out; } catch (e) { console.error('Search error:', e); return []; } } function createCacheKey(editor, searchText, regex, caseSensitive, wholeWord) { return `${editor.getValue().length}⇴${searchText}⇴${regex}⇴${caseSensitive}⇴${wholeWord}`; } const CodeMirrorSearch = forwardRef(({ visible, editor, onClose }, ref) => { const [searchText, setSearchText] = useState(''); const [regex, setRegex] = useState(false); const [caseSensitive, setCaseSensitive] = useState(false); const [wholeWord, setWholeWord] = useState(false); const [matchIndex, setMatchIndex] = useState(0); const [matchCount, setMatchCount] = useState(0); const searchMarks = useRef([]); const searchLineHighlight = useRef(null); const searchMatches = useRef([]); const searchCacheKey = useRef(''); const inputRef = useRef(null); const debouncedSearchText = useDebounce(searchText, 250); const doSearch = useCallback((newIndex = 0) => { if (!editor || !visible) { return; } if (searchLineHighlight.current !== null) { editor.removeLineClass(searchLineHighlight.current, 'wrap', 'cm-search-line-highlight'); searchLineHighlight.current = null; } if (!debouncedSearchText) { setMatchCount(0); setMatchIndex(0); searchMatches.current = []; searchMarks.current.forEach((mark) => mark.clear()); searchMarks.current = []; return; } try { const newCacheKey = createCacheKey(editor, debouncedSearchText, regex, caseSensitive, wholeWord); const isCacheHit = newCacheKey === searchCacheKey.current; let matches = searchMatches.current; if (!isCacheHit) { matches = findSearchMatches(editor, debouncedSearchText, regex, caseSensitive, wholeWord); searchMatches.current = matches; searchCacheKey.current = newCacheKey; setMatchCount(matches.length); } if (!matches.length) { setMatchIndex(0); // Clear previous marks searchMarks.current.forEach((mark) => mark.clear()); searchMarks.current = []; return; } const matchIndex = Math.max(0, Math.min(newIndex, matches.length - 1)); setMatchIndex(matchIndex); if (isCacheHit) { // Clear only old current mark const oldIndex = searchMarks.current.findIndex((mark) => mark.className?.includes('cm-search-current')); if (oldIndex !== -1) { searchMarks.current[oldIndex].clear(); searchMarks.current.splice(oldIndex, 1); } // Add mark to the new current and remark the previous and next const toMark = [ // Previous matchIndex > 0 ? matchIndex - 1 : null, // Current matchIndex, // Next matchIndex < matches.length - 1 ? matchIndex + 1 : null ].filter((i) => i !== null); toMark.forEach((i) => { const mark = editor.markText(matches[i].from, matches[i].to, { className: i === matchIndex ? 'cm-search-current' : 'cm-search-match', clearOnEnter: true }); searchMarks.current.push(mark); }); } else { // Clear previous marks searchMarks.current.forEach((mark) => mark.clear()); searchMarks.current = []; // Mark all on new search matches.forEach((m, i) => { const mark = editor.markText(m.from, m.to, { className: i === matchIndex ? 'cm-search-current' : 'cm-search-match', clearOnEnter: true }); searchMarks.current.push(mark); }); } const currentLine = matches[matchIndex].from.line; editor.addLineClass(currentLine, 'wrap', 'cm-search-line-highlight'); searchLineHighlight.current = currentLine; editor.scrollIntoView(matches[matchIndex].from, 100); editor.setSelection(matches[matchIndex].from, matches[matchIndex].to); } catch (e) { console.error('Search error:', e); setMatchCount(0); setMatchIndex(0); searchMatches.current = []; searchCacheKey.current = ''; } }, [debouncedSearchText, regex, caseSensitive, wholeWord, editor, visible]); useImperativeHandle(ref, () => ({ focus: () => { if (inputRef.current) { inputRef.current.focus(); } } })); useEffect(() => { doSearch(0); }, [debouncedSearchText, doSearch]); const handleSearchBarClose = useCallback(() => { searchMarks.current.forEach((mark) => mark.clear()); searchMarks.current = []; if (searchLineHighlight.current !== null && editor) { editor.removeLineClass(searchLineHighlight.current, 'wrap', 'cm-search-line-highlight'); searchLineHighlight.current = null; } searchMatches.current = []; searchCacheKey.current = ''; if (onClose) onClose(); // Focus the editor after closing the search bar if (editor) { setTimeout(() => editor.focus(), 0); } }, [editor, onClose]); const handleSearchTextChange = (text) => { setSearchText(text); setMatchIndex(0); }; const handleToggleRegex = () => { setRegex((prev) => !prev); setMatchIndex(0); }; const handleToggleCase = () => { setCaseSensitive((prev) => !prev); setMatchIndex(0); }; const handleToggleWholeWord = () => { setWholeWord((prev) => !prev); setMatchIndex(0); }; const handleNext = () => { if (!searchMatches.current || !searchMatches.current.length) return; const next = (matchIndex + 1) % searchMatches.current.length; doSearch(next); }; const handlePrev = () => { if (!searchMatches.current || !searchMatches.current.length) return; const prev = (matchIndex - 1 + searchMatches.current.length) % searchMatches.current.length; doSearch(prev); }; if (!visible) return null; return (
    handleSearchTextChange(e.target.value)} placeholder="Search..." spellCheck={false} onKeyDown={(e) => { if (e.key === 'Enter' && !e.shiftKey) handleNext(); if (e.key === 'Enter' && e.shiftKey) handlePrev(); if (e.key === 'Escape') handleSearchBarClose(); }} /> {matchCount > 0 ? `${matchIndex + 1} / ${matchCount}` : '0 results'}
    ); }); export default CodeMirrorSearch; ================================================ FILE: packages/bruno-app/src/components/CollectionSettings/Auth/ApiKeyAuth/StyledWrapper.js ================================================ import styled from 'styled-components'; const Wrapper = styled.div` label { font-size: ${(props) => props.theme.font.size.sm}; color: ${(props) => props.theme.colors.text.subtext1}; } .single-line-editor-wrapper { padding: 0.15rem 0.4rem; border-radius: 3px; border: solid 1px ${(props) => props.theme.input.border}; background-color: ${(props) => props.theme.input.bg}; } .auth-placement-selector { font-size: ${(props) => props.theme.font.size.sm}; padding: 0.2rem 0px; border-radius: 3px; border: solid 1px ${(props) => props.theme.input.border}; background-color: ${(props) => props.theme.input.bg}; .dropdown { width: fit-content; div[data-tippy-root] { width: fit-content; } .tippy-box { width: fit-content; max-width: none !important; .tippy-content: { width: fit-content; max-width: none !important; } } } .auth-type-label { width: fit-content; justify-content: space-between; padding: 0 0.5rem; } .dropdown-item { padding: 0.2rem 0.6rem !important; } } .caret { color: rgb(140, 140, 140); fill: rgb(140 140 140); } `; export default Wrapper; ================================================ FILE: packages/bruno-app/src/components/CollectionSettings/Auth/ApiKeyAuth/index.js ================================================ import React, { useRef, forwardRef, useEffect } from 'react'; import { useDispatch } from 'react-redux'; import get from 'lodash/get'; import { IconCaretDown } from '@tabler/icons'; import Dropdown from 'components/Dropdown'; import { useTheme } from 'providers/Theme'; import SingleLineEditor from 'components/SingleLineEditor'; import { updateCollectionAuth } from 'providers/ReduxStore/slices/collections'; import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions'; import StyledWrapper from './StyledWrapper'; import { humanizeRequestAPIKeyPlacement } from 'utils/collections'; const ApiKeyAuth = ({ collection }) => { const dispatch = useDispatch(); const { storedTheme } = useTheme(); const dropdownTippyRef = useRef(); const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref); const apikeyAuth = collection.draft?.root ? get(collection, 'draft.root.request.auth.apikey', {}) : get(collection, 'root.request.auth.apikey', {}); const handleSave = () => dispatch(saveCollectionSettings(collection.uid)); const Icon = forwardRef((props, ref) => { return (
    {humanizeRequestAPIKeyPlacement(apikeyAuth?.placement)}
    ); }); const handleAuthChange = (property, value) => { dispatch( updateCollectionAuth({ mode: 'apikey', collectionUid: collection.uid, content: { ...apikeyAuth, [property]: value } }) ); }; useEffect(() => { !apikeyAuth?.placement && dispatch( updateCollectionAuth({ mode: 'apikey', collectionUid: collection.uid, content: { placement: 'header' } }) ); }, [apikeyAuth]); return (
    handleAuthChange('key', val)} collection={collection} isCompact />
    handleAuthChange('value', val)} collection={collection} isCompact />
    } placement="bottom-end">
    { dropdownTippyRef.current.hide(); handleAuthChange('placement', 'header'); }} > Header
    { dropdownTippyRef.current.hide(); handleAuthChange('placement', 'queryparams'); }} > Query Params
    ); }; export default ApiKeyAuth; ================================================ FILE: packages/bruno-app/src/components/CollectionSettings/Auth/AuthMode/StyledWrapper.js ================================================ import styled from 'styled-components'; const Wrapper = styled.div` font-size: ${(props) => props.theme.font.size.base}; .auth-mode-selector { background: transparent; .auth-mode-label { color: ${(props) => props.theme.primary.text}; .caret { color: rgb(140, 140, 140); fill: rgb(140, 140, 140); } } } `; export default Wrapper; ================================================ FILE: packages/bruno-app/src/components/CollectionSettings/Auth/AuthMode/index.js ================================================ import React, { useMemo, useCallback } from 'react'; import get from 'lodash/get'; import { IconCaretDown } from '@tabler/icons'; import MenuDropdown from 'ui/MenuDropdown'; import { useDispatch } from 'react-redux'; import { updateCollectionAuthMode } from 'providers/ReduxStore/slices/collections'; import { humanizeRequestAuthMode } from 'utils/collections'; import StyledWrapper from './StyledWrapper'; const AuthMode = ({ collection }) => { const dispatch = useDispatch(); const authMode = collection.draft?.root ? get(collection, 'draft.root.request.auth.mode') : get(collection, 'root.request.auth.mode'); const onModeChange = useCallback((value) => { dispatch( updateCollectionAuthMode({ collectionUid: collection.uid, mode: value }) ); }, [dispatch, collection.uid]); const menuItems = useMemo(() => [ { id: 'awsv4', label: 'AWS Sig v4', onClick: () => onModeChange('awsv4') }, { id: 'basic', label: 'Basic Auth', onClick: () => onModeChange('basic') }, { id: 'wsse', label: 'WSSE Auth', onClick: () => onModeChange('wsse') }, { id: 'bearer', label: 'Bearer Token', onClick: () => onModeChange('bearer') }, { id: 'digest', label: 'Digest Auth', onClick: () => onModeChange('digest') }, { id: 'ntlm', label: 'NTLM Auth', onClick: () => onModeChange('ntlm') }, { id: 'oauth2', label: 'OAuth 2.0', onClick: () => onModeChange('oauth2') }, { id: 'apikey', label: 'API Key', onClick: () => onModeChange('apikey') }, { id: 'none', label: 'No Auth', onClick: () => onModeChange('none') } ], [onModeChange]); return (
    {humanizeRequestAuthMode(authMode)}
    ); }; export default AuthMode; ================================================ FILE: packages/bruno-app/src/components/CollectionSettings/Auth/AwsV4Auth/StyledWrapper.js ================================================ import styled from 'styled-components'; const Wrapper = styled.div` label { font-size: ${(props) => props.theme.font.size.sm}; color: ${(props) => props.theme.colors.text.subtext1}; } .single-line-editor-wrapper { padding: 0.15rem 0.4rem; border-radius: 3px; border: solid 1px ${(props) => props.theme.input.border}; background-color: ${(props) => props.theme.input.bg}; } `; export default Wrapper; ================================================ FILE: packages/bruno-app/src/components/CollectionSettings/Auth/AwsV4Auth/index.js ================================================ import React from 'react'; import SensitiveFieldWarning from 'components/SensitiveFieldWarning'; import { useDetectSensitiveField } from 'hooks/useDetectSensitiveField'; import get from 'lodash/get'; import { useTheme } from 'providers/Theme'; import { useDispatch } from 'react-redux'; import SingleLineEditor from 'components/SingleLineEditor'; import { updateCollectionAuth } from 'providers/ReduxStore/slices/collections'; import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions'; import StyledWrapper from './StyledWrapper'; const AwsV4Auth = ({ collection }) => { const dispatch = useDispatch(); const { storedTheme } = useTheme(); const awsv4Auth = collection.draft?.root ? get(collection, 'draft.root.request.auth.awsv4', {}) : get(collection, 'root.request.auth.awsv4', {}); const { isSensitive } = useDetectSensitiveField(collection); const { showWarning, warningMessage } = isSensitive(awsv4Auth?.secretAccessKey); const handleSave = () => dispatch(saveCollectionSettings(collection.uid)); const handleAccessKeyIdChange = (accessKeyId) => { dispatch( updateCollectionAuth({ mode: 'awsv4', collectionUid: collection.uid, content: { accessKeyId: accessKeyId || '', secretAccessKey: awsv4Auth.secretAccessKey || '', sessionToken: awsv4Auth.sessionToken || '', service: awsv4Auth.service || '', region: awsv4Auth.region || '', profileName: awsv4Auth.profileName || '' } }) ); }; const handleSecretAccessKeyChange = (secretAccessKey) => { dispatch( updateCollectionAuth({ mode: 'awsv4', collectionUid: collection.uid, content: { accessKeyId: awsv4Auth.accessKeyId || '', secretAccessKey: secretAccessKey || '', sessionToken: awsv4Auth.sessionToken || '', service: awsv4Auth.service || '', region: awsv4Auth.region || '', profileName: awsv4Auth.profileName || '' } }) ); }; const handleSessionTokenChange = (sessionToken) => { dispatch( updateCollectionAuth({ mode: 'awsv4', collectionUid: collection.uid, content: { accessKeyId: awsv4Auth.accessKeyId || '', secretAccessKey: awsv4Auth.secretAccessKey || '', sessionToken: sessionToken || '', service: awsv4Auth.service || '', region: awsv4Auth.region || '', profileName: awsv4Auth.profileName || '' } }) ); }; const handleServiceChange = (service) => { dispatch( updateCollectionAuth({ mode: 'awsv4', collectionUid: collection.uid, content: { accessKeyId: awsv4Auth.accessKeyId || '', secretAccessKey: awsv4Auth.secretAccessKey || '', sessionToken: awsv4Auth.sessionToken || '', service: service || '', region: awsv4Auth.region || '', profileName: awsv4Auth.profileName || '' } }) ); }; const handleRegionChange = (region) => { dispatch( updateCollectionAuth({ mode: 'awsv4', collectionUid: collection.uid, content: { accessKeyId: awsv4Auth.accessKeyId || '', secretAccessKey: awsv4Auth.secretAccessKey || '', sessionToken: awsv4Auth.sessionToken || '', service: awsv4Auth.service || '', region: region || '', profileName: awsv4Auth.profileName || '' } }) ); }; const handleProfileNameChange = (profileName) => { dispatch( updateCollectionAuth({ mode: 'awsv4', collectionUid: collection.uid, content: { accessKeyId: awsv4Auth.accessKeyId || '', secretAccessKey: awsv4Auth.secretAccessKey || '', sessionToken: awsv4Auth.sessionToken || '', service: awsv4Auth.service || '', region: awsv4Auth.region || '', profileName: profileName || '' } }) ); }; return (
    handleAccessKeyIdChange(val)} collection={collection} isCompact />
    handleSecretAccessKeyChange(val)} collection={collection} isSecret={true} isCompact /> {showWarning && }
    handleSessionTokenChange(val)} collection={collection} isCompact />
    handleServiceChange(val)} collection={collection} isCompact />
    handleRegionChange(val)} collection={collection} isCompact />
    handleProfileNameChange(val)} collection={collection} isCompact />
    ); }; export default AwsV4Auth; ================================================ FILE: packages/bruno-app/src/components/CollectionSettings/Auth/BasicAuth/StyledWrapper.js ================================================ import styled from 'styled-components'; const Wrapper = styled.div` label { font-size: ${(props) => props.theme.font.size.sm}; color: ${(props) => props.theme.colors.text.subtext1}; } .single-line-editor-wrapper { padding: 0.15rem 0.4rem; border-radius: 3px; border: solid 1px ${(props) => props.theme.input.border}; background-color: ${(props) => props.theme.input.bg}; } `; export default Wrapper; ================================================ FILE: packages/bruno-app/src/components/CollectionSettings/Auth/BasicAuth/index.js ================================================ import React from 'react'; import SensitiveFieldWarning from 'components/SensitiveFieldWarning'; import { useDetectSensitiveField } from 'hooks/useDetectSensitiveField'; import get from 'lodash/get'; import { useTheme } from 'providers/Theme'; import { useDispatch } from 'react-redux'; import SingleLineEditor from 'components/SingleLineEditor'; import { updateCollectionAuth } from 'providers/ReduxStore/slices/collections'; import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions'; import StyledWrapper from './StyledWrapper'; const BasicAuth = ({ collection }) => { const dispatch = useDispatch(); const { storedTheme } = useTheme(); const basicAuth = collection.draft?.root ? get(collection, 'draft.root.request.auth.basic', {}) : get(collection, 'root.request.auth.basic', {}); const { isSensitive } = useDetectSensitiveField(collection); const { showWarning, warningMessage } = isSensitive(basicAuth?.password); const handleSave = () => dispatch(saveCollectionSettings(collection.uid)); const handleUsernameChange = (username) => { dispatch( updateCollectionAuth({ mode: 'basic', collectionUid: collection.uid, content: { username: username || '', password: basicAuth.password || '' } }) ); }; const handlePasswordChange = (password) => { dispatch( updateCollectionAuth({ mode: 'basic', collectionUid: collection.uid, content: { username: basicAuth.username || '', password: password || '' } }) ); }; return (
    handleUsernameChange(val)} collection={collection} isCompact />
    handlePasswordChange(val)} collection={collection} isSecret={true} isCompact /> {showWarning && }
    ); }; export default BasicAuth; ================================================ FILE: packages/bruno-app/src/components/CollectionSettings/Auth/BearerAuth/StyledWrapper.js ================================================ import styled from 'styled-components'; const Wrapper = styled.div` label { font-size: ${(props) => props.theme.font.size.sm}; color: ${(props) => props.theme.colors.text.subtext1}; } .single-line-editor-wrapper { padding: 0.15rem 0.4rem; border-radius: 3px; border: solid 1px ${(props) => props.theme.input.border}; background-color: ${(props) => props.theme.input.bg}; } `; export default Wrapper; ================================================ FILE: packages/bruno-app/src/components/CollectionSettings/Auth/BearerAuth/index.js ================================================ import React from 'react'; import SensitiveFieldWarning from 'components/SensitiveFieldWarning'; import { useDetectSensitiveField } from 'hooks/useDetectSensitiveField'; import get from 'lodash/get'; import { useTheme } from 'providers/Theme'; import { useDispatch } from 'react-redux'; import SingleLineEditor from 'components/SingleLineEditor'; import { updateCollectionAuth } from 'providers/ReduxStore/slices/collections'; import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions'; import StyledWrapper from './StyledWrapper'; const BearerAuth = ({ collection }) => { const dispatch = useDispatch(); const { storedTheme } = useTheme(); const bearerToken = collection.draft?.root ? get(collection, 'draft.root.request.auth.bearer.token', '') : get(collection, 'root.request.auth.bearer.token', ''); const { isSensitive } = useDetectSensitiveField(collection); const { showWarning, warningMessage } = isSensitive(bearerToken); const handleSave = () => dispatch(saveCollectionSettings(collection.uid)); const handleTokenChange = (token) => { dispatch( updateCollectionAuth({ mode: 'bearer', collectionUid: collection.uid, content: { token: token } }) ); }; return (
    handleTokenChange(val)} collection={collection} isSecret={true} isCompact /> {showWarning && }
    ); }; export default BearerAuth; ================================================ FILE: packages/bruno-app/src/components/CollectionSettings/Auth/DigestAuth/StyledWrapper.js ================================================ import styled from 'styled-components'; const Wrapper = styled.div` label { font-size: ${(props) => props.theme.font.size.sm}; color: ${(props) => props.theme.colors.text.subtext1}; } .single-line-editor-wrapper { padding: 0.15rem 0.4rem; border-radius: 3px; border: solid 1px ${(props) => props.theme.input.border}; background-color: ${(props) => props.theme.input.bg}; } `; export default Wrapper; ================================================ FILE: packages/bruno-app/src/components/CollectionSettings/Auth/DigestAuth/index.js ================================================ import React from 'react'; import SensitiveFieldWarning from 'components/SensitiveFieldWarning'; import { useDetectSensitiveField } from 'hooks/useDetectSensitiveField'; import get from 'lodash/get'; import { useTheme } from 'providers/Theme'; import { useDispatch } from 'react-redux'; import SingleLineEditor from 'components/SingleLineEditor'; import { updateCollectionAuth } from 'providers/ReduxStore/slices/collections'; import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions'; import StyledWrapper from './StyledWrapper'; const DigestAuth = ({ collection }) => { const dispatch = useDispatch(); const { storedTheme } = useTheme(); const digestAuth = collection.draft?.root ? get(collection, 'draft.root.request.auth.digest', {}) : get(collection, 'root.request.auth.digest', {}); const { isSensitive } = useDetectSensitiveField(collection); const { showWarning, warningMessage } = isSensitive(digestAuth?.password); const handleSave = () => dispatch(saveCollectionSettings(collection.uid)); const handleUsernameChange = (username) => { dispatch( updateCollectionAuth({ mode: 'digest', collectionUid: collection.uid, content: { username: username || '', password: digestAuth.password || '' } }) ); }; const handlePasswordChange = (password) => { dispatch( updateCollectionAuth({ mode: 'digest', collectionUid: collection.uid, content: { username: digestAuth.username || '', password: password || '' } }) ); }; return (
    handleUsernameChange(val)} collection={collection} isCompact />
    handlePasswordChange(val)} collection={collection} isSecret={true} isCompact /> {showWarning && }
    ); }; export default DigestAuth; ================================================ FILE: packages/bruno-app/src/components/CollectionSettings/Auth/NTLMAuth/StyledWrapper.js ================================================ import styled from 'styled-components'; const Wrapper = styled.div` label { font-size: ${(props) => props.theme.font.size.sm}; color: ${(props) => props.theme.colors.text.subtext1}; } .single-line-editor-wrapper { max-width: 400px; padding: 0.15rem 0.4rem; border-radius: 3px; border: solid 1px ${(props) => props.theme.input.border}; background-color: ${(props) => props.theme.input.bg}; } `; export default Wrapper; ================================================ FILE: packages/bruno-app/src/components/CollectionSettings/Auth/NTLMAuth/index.js ================================================ import React from 'react'; import SensitiveFieldWarning from 'components/SensitiveFieldWarning'; import { useDetectSensitiveField } from 'hooks/useDetectSensitiveField'; import get from 'lodash/get'; import { useTheme } from 'providers/Theme'; import { useDispatch } from 'react-redux'; import SingleLineEditor from 'components/SingleLineEditor'; import { updateCollectionAuth } from 'providers/ReduxStore/slices/collections'; import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions'; import StyledWrapper from './StyledWrapper'; const NTLMAuth = ({ collection }) => { const dispatch = useDispatch(); const { storedTheme } = useTheme(); const ntlmAuth = collection.draft?.root ? get(collection, 'draft.root.request.auth.ntlm', {}) : get(collection, 'root.request.auth.ntlm', {}); const { isSensitive } = useDetectSensitiveField(collection); const { showWarning, warningMessage } = isSensitive(ntlmAuth?.password); const handleSave = () => dispatch(saveCollectionSettings(collection.uid)); const handleUsernameChange = (username) => { dispatch( updateCollectionAuth({ mode: 'ntlm', collectionUid: collection.uid, content: { username: username || '', password: ntlmAuth.password || '', domain: ntlmAuth.domain || '' } }) ); }; const handlePasswordChange = (password) => { dispatch( updateCollectionAuth({ mode: 'ntlm', collectionUid: collection.uid, content: { username: ntlmAuth.username || '', password: password || '', domain: ntlmAuth.domain || '' } }) ); }; const handleDomainChange = (domain) => { dispatch( updateCollectionAuth({ mode: 'ntlm', collectionUid: collection.uid, content: { username: ntlmAuth.username || '', password: ntlmAuth.password || '', domain: domain || '' } }) ); }; return (
    handleUsernameChange(val)} collection={collection} isCompact />
    handlePasswordChange(val)} collection={collection} isSecret={true} isCompact /> {showWarning && }
    handleDomainChange(val)} collection={collection} isCompact />
    ); }; export default NTLMAuth; ================================================ FILE: packages/bruno-app/src/components/CollectionSettings/Auth/OAuth2/StyledWrapper.js ================================================ import styled from 'styled-components'; const Wrapper = styled.div` label { font-size: ${(props) => props.theme.font.size.base}; } .single-line-editor-wrapper { max-width: 400px; padding: 0.15rem 0.4rem; border-radius: 3px; border: solid 1px ${(props) => props.theme.input.border}; background-color: ${(props) => props.theme.input.bg}; } `; export default Wrapper; ================================================ FILE: packages/bruno-app/src/components/CollectionSettings/Auth/OAuth2/index.js ================================================ import React from 'react'; import get from 'lodash/get'; import StyledWrapper from './StyledWrapper'; import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions'; import OAuth2AuthorizationCode from 'components/RequestPane/Auth/OAuth2/AuthorizationCode/index'; import { updateCollectionAuth } from 'providers/ReduxStore/slices/collections/index'; import { useDispatch } from 'react-redux'; import OAuth2PasswordCredentials from 'components/RequestPane/Auth/OAuth2/PasswordCredentials/index'; import OAuth2ClientCredentials from 'components/RequestPane/Auth/OAuth2/ClientCredentials/index'; import OAuth2Implicit from 'components/RequestPane/Auth/OAuth2/Implicit/index'; import GrantTypeSelector from 'components/RequestPane/Auth/OAuth2/GrantTypeSelector/index'; const GrantTypeComponentMap = ({ collection }) => { const dispatch = useDispatch(); const save = () => { dispatch(saveCollectionSettings(collection.uid)); }; let request = collection.draft?.root ? get(collection, 'draft.root.request', {}) : get(collection, 'root.request', {}); const grantType = get(request, 'auth.oauth2.grantType', {}); switch (grantType) { case 'password': return ; break; case 'authorization_code': return ; break; case 'client_credentials': return ; break; case 'implicit': return ; break; default: return
    TBD
    ; break; } }; const OAuth2 = ({ collection }) => { let request = collection.draft?.root ? get(collection, 'draft.root.request', {}) : get(collection, 'root.request', {}); return ( ); }; export default OAuth2; ================================================ FILE: packages/bruno-app/src/components/CollectionSettings/Auth/StyledWrapper.js ================================================ import styled from 'styled-components'; const Wrapper = styled.div` max-width: 800px; `; export default Wrapper; ================================================ FILE: packages/bruno-app/src/components/CollectionSettings/Auth/WsseAuth/StyledWrapper.js ================================================ import styled from 'styled-components'; const Wrapper = styled.div` label { font-size: ${(props) => props.theme.font.size.sm}; color: ${(props) => props.theme.colors.text.subtext1}; } .single-line-editor-wrapper { padding: 0.15rem 0.4rem; border-radius: 3px; border: solid 1px ${(props) => props.theme.input.border}; background-color: ${(props) => props.theme.input.bg}; } `; export default Wrapper; ================================================ FILE: packages/bruno-app/src/components/CollectionSettings/Auth/WsseAuth/index.js ================================================ import React from 'react'; import SensitiveFieldWarning from 'components/SensitiveFieldWarning'; import { useDetectSensitiveField } from 'hooks/useDetectSensitiveField'; import get from 'lodash/get'; import { useTheme } from 'providers/Theme'; import { useDispatch } from 'react-redux'; import SingleLineEditor from 'components/SingleLineEditor'; import { updateCollectionAuth } from 'providers/ReduxStore/slices/collections'; import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions'; import StyledWrapper from './StyledWrapper'; const WsseAuth = ({ collection }) => { const dispatch = useDispatch(); const { storedTheme } = useTheme(); const wsseAuth = collection.draft?.root ? get(collection, 'draft.root.request.auth.wsse', {}) : get(collection, 'root.request.auth.wsse', {}); const { isSensitive } = useDetectSensitiveField(collection); const { showWarning, warningMessage } = isSensitive(wsseAuth?.password); const handleSave = () => dispatch(saveCollectionSettings(collection.uid)); const handleUserChange = (username) => { dispatch( updateCollectionAuth({ mode: 'wsse', collectionUid: collection.uid, content: { username: username || '', password: wsseAuth.password || '' } }) ); }; const handlePasswordChange = (password) => { dispatch( updateCollectionAuth({ mode: 'wsse', collectionUid: collection.uid, content: { username: wsseAuth.username || '', password: password || '' } }) ); }; return (
    handleUserChange(val)} collection={collection} isCompact />
    handlePasswordChange(val)} collection={collection} isSecret={true} isCompact /> {showWarning && }
    ); }; export default WsseAuth; ================================================ FILE: packages/bruno-app/src/components/CollectionSettings/Auth/index.js ================================================ import React from 'react'; import get from 'lodash/get'; import { useDispatch } from 'react-redux'; import AuthMode from './AuthMode'; import AwsV4Auth from './AwsV4Auth'; import BearerAuth from './BearerAuth'; import BasicAuth from './BasicAuth'; import DigestAuth from './DigestAuth'; import WsseAuth from './WsseAuth'; import ApiKeyAuth from './ApiKeyAuth/'; import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions'; import StyledWrapper from './StyledWrapper'; import OAuth2 from './OAuth2'; import NTLMAuth from './NTLMAuth'; import Button from 'ui/Button'; const Auth = ({ collection }) => { const authMode = collection.draft?.root ? get(collection, 'draft.root.request.auth.mode') : get(collection, 'root.request.auth.mode'); const dispatch = useDispatch(); const handleSave = () => dispatch(saveCollectionSettings(collection.uid)); const getAuthView = () => { switch (authMode) { case 'awsv4': { return ; } case 'basic': { return ; } case 'bearer': { return ; } case 'digest': { return ; } case 'ntlm': { return ; } case 'oauth2': { return ; } case 'wsse': { return ; } case 'apikey': { return ; } } }; return (
    Configures authentication for the entire collection. This applies to all requests using the{' '} Inherit option in the Auth tab.
    {getAuthView()}
    ); }; export default Auth; ================================================ FILE: packages/bruno-app/src/components/CollectionSettings/ClientCertSettings/StyledWrapper.js ================================================ import styled from 'styled-components'; const StyledWrapper = styled.div` .settings-label { width: 90px; } .certificate-icon { color: ${(props) => props.theme.colors.text.yellow}; } .non-passphrase-input { width: 300px; } .available-certificates { background-color: ${(props) => props.theme.requestTabPanel.url.bg}; button.remove-certificate { color: ${(props) => props.theme.colors.text.danger}; } } .textbox { border: 1px solid #ccc; padding: 0.15rem 0.45rem; box-shadow: none; border-radius: 0px; outline: none; box-shadow: none; transition: border-color ease-in-out 0.1s; border-radius: 3px; background-color: ${(props) => props.theme.input.bg}; border: 1px solid ${(props) => props.theme.input.border}; &:focus { border: solid 1px ${(props) => props.theme.input.focusBorder} !important; outline: none !important; } } .protocol-placeholder { height: 100%; position: relative; display: inline-block; width: 60px; overflow: hidden; } .protocol-https, .protocol-grpcs, .protocol-wss { position: absolute; right: 8px; top: 0; bottom: 0; display: flex; align-items: center; justify-content: center; } .protocol-https { animation: slideUpDown 9s infinite; transform: translateY(0); } .protocol-grpcs { animation: slideUpDown 9s infinite 3s; transform: translateY(100%); } .protocol-wss { animation: slideUpDown 9s infinite 6s; transform: translateY(100%); } @keyframes slideUpDown { 0%, 30% { transform: translateY(0); } 33.33%, 97% { transform: translateY(100%); } 100% { transform: translateY(0); } } `; export default StyledWrapper; ================================================ FILE: packages/bruno-app/src/components/CollectionSettings/ClientCertSettings/index.js ================================================ import React from 'react'; import { IconCertificate, IconTrash, IconWorld } from '@tabler/icons'; import { useFormik } from 'formik'; import * as Yup from 'yup'; import StyledWrapper from './StyledWrapper'; import { useRef } from 'react'; import path from 'utils/common/path'; import SensitiveFieldWarning from 'components/SensitiveFieldWarning/index'; import SingleLineEditor from 'components/SingleLineEditor/index'; import { useDetectSensitiveField } from 'hooks/useDetectSensitiveField/index'; import { useTheme } from 'styled-components'; import { useDispatch } from 'react-redux'; import { updateCollectionClientCertificates } from 'providers/ReduxStore/slices/collections'; import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions'; import get from 'lodash/get'; import Button from 'ui/Button'; const ClientCertSettings = ({ collection }) => { const dispatch = useDispatch(); // Get client certs from draft if exists, otherwise from brunoConfig const clientCertConfig = collection.draft?.brunoConfig ? get(collection, 'draft.brunoConfig.clientCertificates.certs', []) : get(collection, 'brunoConfig.clientCertificates.certs', []); const certFilePathInputRef = useRef(); const keyFilePathInputRef = useRef(); const pfxFilePathInputRef = useRef(); const { storedTheme } = useTheme(); const formik = useFormik({ initialValues: { domain: '', type: 'cert', certFilePath: '', keyFilePath: '', pfxFilePath: '', passphrase: '' }, validationSchema: Yup.object({ domain: Yup.string() .required() .trim() .test('not-empty-after-trim', 'Domain is required', (value) => value && value.trim().length > 0), type: Yup.string().required().oneOf(['cert', 'pfx']), certFilePath: Yup.string().when('type', { is: (type) => type == 'cert', then: Yup.string().min(1, 'certFilePath is a required field').required() }), keyFilePath: Yup.string().when('type', { is: (type) => type == 'cert', then: Yup.string().min(1, 'keyFilePath is a required field').required() }), pfxFilePath: Yup.string().when('type', { is: (type) => type == 'pfx', then: Yup.string().min(1, 'pfxFilePath is a required field').required() }), passphrase: Yup.string() }), onSubmit: (values) => { let relevantValues = {}; if (values.type === 'cert') { relevantValues = { domain: values.domain?.trim(), type: values.type, certFilePath: values.certFilePath, keyFilePath: values.keyFilePath, passphrase: values.passphrase }; } else { relevantValues = { domain: values.domain?.trim(), type: values.type, pfxFilePath: values.pfxFilePath, passphrase: values.passphrase }; } // Add the new cert to the existing certs in draft const updatedCerts = [...clientCertConfig, relevantValues]; const clientCertificates = { enabled: true, certs: updatedCerts }; dispatch(updateCollectionClientCertificates({ collectionUid: collection.uid, clientCertificates })); formik.resetForm(); resetFileInputFields(); } }); const { isSensitive } = useDetectSensitiveField(collection); const { showWarning, warningMessage } = isSensitive(formik.values.passphrase); const getFile = (e) => { const filePath = window?.ipcRenderer?.getFilePath(e?.files?.[0]); if (filePath) { let relativePath = path.relative(collection.pathname, filePath); formik.setFieldValue(e.name, relativePath); } }; const resetFileInputFields = () => { if (certFilePathInputRef.current) { certFilePathInputRef.current.value = ''; } if (keyFilePathInputRef.current) { keyFilePathInputRef.current.value = ''; } if (pfxFilePathInputRef.current) { pfxFilePathInputRef.current.value = ''; } }; const handleTypeChange = (e) => { formik.setFieldValue('type', e.target.value); if (e.target.value === 'cert') { formik.setFieldValue('pfxFilePath', ''); pfxFilePathInputRef.current.value = ''; } else { formik.setFieldValue('certFilePath', ''); certFilePathInputRef.current.value = ''; formik.setFieldValue('keyFilePath', ''); keyFilePathInputRef.current.value = ''; } }; const handleRemove = (indexToRemove) => { const updatedCerts = clientCertConfig.filter((cert, index) => index !== indexToRemove); const clientCertificates = { enabled: true, certs: updatedCerts }; dispatch(updateCollectionClientCertificates({ collectionUid: collection.uid, clientCertificates })); }; const handleSave = () => dispatch(saveCollectionSettings(collection.uid)); return (
    Add client certificates to be used for specific domains.

    Client Certificates

      {!clientCertConfig.length ? 'No client certificates added' : clientCertConfig.map((clientCert, index) => (
    • {clientCert.domain}
      {clientCert.type === 'cert' ? clientCert.certFilePath : clientCert.pfxFilePath}
    • ))}

    Add Client Certificate

    https:// grpcs:// wss://
    {formik.touched.domain && formik.errors.domain ? (
    {formik.errors.domain}
    ) : null}
    {formik.values.type === 'cert' ? ( <>
    getFile(e.target)} ref={certFilePathInputRef} /> {formik.values.certFilePath ? (
    {path.basename(formik.values.certFilePath)}
    { formik.setFieldValue('certFilePath', ''); certFilePathInputRef.current.value = ''; }} />
    ) : ( <> )}
    {formik.touched.certFilePath && formik.errors.certFilePath ? (
    {formik.errors.certFilePath}
    ) : null}
    getFile(e.target)} ref={keyFilePathInputRef} /> {formik.values.keyFilePath ? (
    {path.basename(formik.values.keyFilePath)}
    { formik.setFieldValue('keyFilePath', ''); keyFilePathInputRef.current.value = ''; }} />
    ) : ( <> )}
    {formik.touched.keyFilePath && formik.errors.keyFilePath ? (
    {formik.errors.keyFilePath}
    ) : null}
    ) : ( <>
    getFile(e.target)} ref={pfxFilePathInputRef} /> {formik.values.pfxFilePath ? (
    {path.basename(formik.values.pfxFilePath)}
    { formik.setFieldValue('pfxFilePath', ''); pfxFilePathInputRef.current.value = ''; }} />
    ) : ( <> )}
    {formik.touched.pfxFilePath && formik.errors.pfxFilePath ? (
    {formik.errors.pfxFilePath}
    ) : null}
    )}
    formik.setFieldValue('passphrase', val)} collection={collection} isSecret={true} /> {showWarning && }
    {formik.touched.passphrase && formik.errors.passphrase ? (
    {formik.errors.passphrase}
    ) : null}
    ); }; export default ClientCertSettings; ================================================ FILE: packages/bruno-app/src/components/CollectionSettings/Docs/StyledWrapper.js ================================================ import styled from 'styled-components'; const StyledWrapper = styled.div` .editing-mode { cursor: pointer; } `; export default StyledWrapper; ================================================ FILE: packages/bruno-app/src/components/CollectionSettings/Docs/index.js ================================================ import 'github-markdown-css/github-markdown.css'; import get from 'lodash/get'; import { updateCollectionDocs, deleteCollectionDraft } from 'providers/ReduxStore/slices/collections'; import { useTheme } from 'providers/Theme'; import { useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions'; import Markdown from 'components/MarkDown'; import CodeEditor from 'components/CodeEditor'; import StyledWrapper from './StyledWrapper'; import { IconEdit, IconX, IconFileText } from '@tabler/icons'; import Button from 'ui/Button/index'; import ActionIcon from 'ui/ActionIcon/index'; const Docs = ({ collection }) => { const dispatch = useDispatch(); const { displayedTheme } = useTheme(); const [isEditing, setIsEditing] = useState(false); const docs = collection.draft?.root ? get(collection, 'draft.root.docs', '') : get(collection, 'root.docs', ''); const preferences = useSelector((state) => state.app.preferences); const toggleViewMode = () => { setIsEditing((prev) => !prev); }; const onEdit = (value) => { dispatch( updateCollectionDocs({ collectionUid: collection.uid, docs: value }) ); }; const handleDiscardChanges = () => { dispatch(( updateCollectionDocs({ collectionUid: collection.uid, docs: docs })) ); toggleViewMode(); }; const onSave = () => { dispatch(saveCollectionSettings(collection.uid)); toggleViewMode(); }; return (
    Documentation
    {isEditing ? ( <> ) : ( )}
    {isEditing ? ( ) : (
    { docs?.length > 0 ? : }
    )}
    ); }; export default Docs; const documentationPlaceholder = ` Welcome to your collection documentation! This space is designed to help you document your API collection effectively. ## Overview Use this section to provide a high-level overview of your collection. You can describe: - The purpose of these API endpoints - Key features and functionalities - Target audience or users ## Best Practices - Keep documentation up to date - Include request/response examples - Document error scenarios - Add relevant links and references ## Markdown Support This documentation supports Markdown formatting! You can use: - **Bold** and *italic* text - \`code blocks\` and syntax highlighting - Tables and lists - [Links](https://usebruno.com) - And more! `; ================================================ FILE: packages/bruno-app/src/components/CollectionSettings/Headers/StyledWrapper.js ================================================ import styled from 'styled-components'; const Wrapper = styled.div` max-width: 800px; table { width: 100%; border-collapse: collapse; font-weight: 500; table-layout: fixed; thead, td { border: 1px solid ${(props) => props.theme.table.border}; } thead { color: ${(props) => props.theme.table.thead.color}; font-size: ${(props) => props.theme.font.size.base}; user-select: none; } td { padding: 6px 10px; &:nth-child(1) { width: 30%; } &:nth-child(3) { width: 70px; } } } .btn-add-header { font-size: ${(props) => props.theme.font.size.base}; } input[type='text'] { width: 100%; border: solid 1px transparent; outline: none !important; background-color: inherit; &:focus { outline: none !important; border: solid 1px transparent; } } input[type='checkbox'] { cursor: pointer; position: relative; top: 1px; } `; export default Wrapper; ================================================ FILE: packages/bruno-app/src/components/CollectionSettings/Headers/index.js ================================================ import React, { useState, useCallback } from 'react'; import get from 'lodash/get'; import { useDispatch } from 'react-redux'; import { useTheme } from 'providers/Theme'; import { setCollectionHeaders } from 'providers/ReduxStore/slices/collections'; import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions'; import SingleLineEditor from 'components/SingleLineEditor'; import EditableTable from 'components/EditableTable'; import StyledWrapper from './StyledWrapper'; import { headers as StandardHTTPHeaders } from 'know-your-http-well'; import { MimeTypes } from 'utils/codemirror/autocompleteConstants'; import BulkEditor from 'components/BulkEditor/index'; import Button from 'ui/Button'; import { headerNameRegex, headerValueRegex } from 'utils/common/regex'; const headerAutoCompleteList = StandardHTTPHeaders.map((e) => e.header); const Headers = ({ collection }) => { const dispatch = useDispatch(); const { storedTheme } = useTheme(); const headers = collection.draft?.root ? get(collection, 'draft.root.request.headers', []) : get(collection, 'root.request.headers', []); const [isBulkEditMode, setIsBulkEditMode] = useState(false); const toggleBulkEditMode = () => { setIsBulkEditMode(!isBulkEditMode); }; const handleHeadersChange = useCallback((updatedHeaders) => { dispatch(setCollectionHeaders({ collectionUid: collection.uid, headers: updatedHeaders })); }, [dispatch, collection.uid]); const handleSave = () => dispatch(saveCollectionSettings(collection.uid)); const getRowError = useCallback((row, index, key) => { if (key === 'name') { if (!row.name || row.name.trim() === '') return null; if (!headerNameRegex.test(row.name)) { return 'Header name cannot contain spaces or newlines'; } } if (key === 'value') { if (!row.value) return null; if (!headerValueRegex.test(row.value)) { return 'Header value cannot contain newlines'; } } return null; }, []); const columns = [ { key: 'name', name: 'Name', isKeyField: true, placeholder: 'Name', width: '30%', render: ({ value, onChange }) => ( onChange(newValue.replace(/[\r\n]/g, ''))} autocomplete={headerAutoCompleteList} collection={collection} placeholder={!value ? 'Name' : ''} /> ) }, { key: 'value', name: 'Value', placeholder: 'Value', render: ({ value, onChange }) => ( ) } ]; const defaultRow = { name: '', value: '', description: '' }; if (isBulkEditMode) { return (
    Add request headers that will be sent with every request in this collection.
    ); } return (
    Add request headers that will be sent with every request in this collection.
    ); }; export default Headers; ================================================ FILE: packages/bruno-app/src/components/CollectionSettings/Overview/Info/StyledWrapper.js ================================================ import styled from 'styled-components'; import { rgba } from 'polished'; const StyledWrapper = styled.div` .icon-box { &.location { background-color: ${(props) => rgba(props.theme.textLink, 0.08)}; border: 1px solid ${(props) => rgba(props.theme.textLink, 0.09)}; svg { color: ${(props) => props.theme.textLink}; } } &.environments { background-color: ${(props) => rgba(props.theme.colors.text.green, 0.08)}; border: 1px solid ${(props) => rgba(props.theme.colors.text.green, 0.09)}; svg { color: ${(props) => props.theme.colors.text.green}; } } &.requests { background-color: ${(props) => rgba(props.theme.colors.text.purple, 0.08)}; border: 1px solid ${(props) => rgba(props.theme.colors.text.purple, 0.09)}; svg { color: ${(props) => props.theme.colors.text.purple}; } } &.share { background-color: ${(props) => rgba(props.theme.textLink, 0.08)}; border: 1px solid ${(props) => rgba(props.theme.textLink, 0.09)}; svg { color: ${(props) => props.theme.textLink}; } } &.generate-docs { background-color: ${(props) => rgba(props.theme.accents.primary, 0.08)}; border: 1px solid ${(props) => rgba(props.theme.accents.primary, 0.09)}; svg { color: ${(props) => props.theme.accents.primary}; } } } `; export default StyledWrapper; ================================================ FILE: packages/bruno-app/src/components/CollectionSettings/Overview/Info/index.js ================================================ import React from 'react'; import { getTotalRequestCountInCollection } from 'utils/collections/'; import { IconFolder, IconWorld, IconApi, IconShare, IconBook } from '@tabler/icons'; import { areItemsLoading, getItemsLoadStats } from 'utils/collections/index'; import { useState } from 'react'; import { useSelector, useDispatch } from 'react-redux'; import ShareCollection from 'components/ShareCollection/index'; import GenerateDocumentation from 'components/Sidebar/Collections/Collection/GenerateDocumentation'; import { addTab } from 'providers/ReduxStore/slices/tabs'; import StyledWrapper from './StyledWrapper'; const Info = ({ collection }) => { const dispatch = useDispatch(); const totalRequestsInCollection = getTotalRequestCountInCollection(collection); const isCollectionLoading = areItemsLoading(collection); const { loading: itemsLoadingCount, total: totalItems } = getItemsLoadStats(collection); const [showShareCollectionModal, toggleShowShareCollectionModal] = useState(false); const [showGenerateDocumentationModal, setShowGenerateDocumentationModal] = useState(false); const globalEnvironments = useSelector((state) => state.globalEnvironments.globalEnvironments); const collectionEnvironmentCount = collection.environments?.length || 0; const globalEnvironmentCount = globalEnvironments?.length || 0; const handleToggleShowShareCollectionModal = (value) => (e) => { toggleShowShareCollectionModal(value); }; return (
    {/* Location Row */}
    Location
    {collection.pathname}
    {/* Environments Row */}
    Environments
    {/* Requests Row */}
    Requests
    { isCollectionLoading ? `${totalItems - itemsLoadingCount} out of ${totalItems} requests in the collection loaded` : `${totalRequestsInCollection} request${totalRequestsInCollection !== 1 ? 's' : ''} in collection` }
    Share
    Share Collection
    {showShareCollectionModal && }
    setShowGenerateDocumentationModal(true)}>
    Documentation
    Generate Docs
    {showGenerateDocumentationModal && setShowGenerateDocumentationModal(false)} />}
    ); }; export default Info; ================================================ FILE: packages/bruno-app/src/components/CollectionSettings/Overview/RequestsNotLoaded/StyledWrapper.js ================================================ import styled from 'styled-components'; import { rgba } from 'polished'; const StyledWrapper = styled.div` &.card { background-color: ${(props) => props.theme.requestTabPanel.card.bg}; .title { border-top: 1px solid ${(props) => props.theme.table.border}; border-left: 1px solid ${(props) => props.theme.table.border}; border-right: 1px solid ${(props) => props.theme.table.border}; border-top-left-radius: 3px; border-top-right-radius: 3px; background-color: ${(props) => props.theme.status.warning.background}; } .warning-icon { color: ${(props) => props.theme.status.warning.text}; } .table { thead { color: ${(props) => props.theme.table.thead.color} !important; background: ${(props) => props.theme.sidebar.bg}; } } } `; export default StyledWrapper; ================================================ FILE: packages/bruno-app/src/components/CollectionSettings/Overview/RequestsNotLoaded/index.js ================================================ import React from 'react'; import { flattenItems } from 'utils/collections'; import { IconAlertTriangle } from '@tabler/icons'; import StyledWrapper from './StyledWrapper'; import { useDispatch, useSelector } from 'react-redux'; import { isItemARequest, itemIsOpenedInTabs } from 'utils/tabs/index'; import { getDefaultRequestPaneTab } from 'utils/collections/index'; import { addTab, focusTab } from 'providers/ReduxStore/slices/tabs'; const RequestsNotLoaded = ({ collection }) => { const dispatch = useDispatch(); const tabs = useSelector((state) => state.tabs.tabs); const flattenedItems = flattenItems(collection.items); const itemsFailedLoading = flattenedItems?.filter((item) => item?.partial && !item?.loading); if (!itemsFailedLoading?.length) { return null; } const handleRequestClick = (item) => (e) => { e.preventDefault(); if (isItemARequest(item)) { if (itemIsOpenedInTabs(item, tabs)) { dispatch( focusTab({ uid: item.uid }) ); return; } dispatch( addTab({ uid: item.uid, collectionUid: collection.uid, requestPaneTab: getDefaultRequestPaneTab(item) }) ); return; } }; return (
    Following requests were not loaded
    {flattenedItems?.map((item, index) => ( item?.partial && !item?.loading ? ( ) : null ))}
    Pathname Size
    {item?.pathname?.split(`${collection?.pathname}/`)?.[1]} {item?.size?.toFixed?.(2)} MB
    ); }; export default RequestsNotLoaded; ================================================ FILE: packages/bruno-app/src/components/CollectionSettings/Overview/StyledWrapper.js ================================================ import styled from 'styled-components'; const StyledWrapper = styled.div` .partial { color: ${(props) => props.theme.colors.text.yellow}; opacity: 0.8; } .loading { color: ${(props) => props.theme.colors.text.muted}; opacity: 0.8; } .completed { color: ${(props) => props.theme.colors.text.green}; opacity: 0.8; } .failed { color: ${(props) => props.theme.colors.text.danger}; opacity: 0.8; } `; export default StyledWrapper; ================================================ FILE: packages/bruno-app/src/components/CollectionSettings/Overview/index.js ================================================ import StyledWrapper from './StyledWrapper'; import Docs from '../Docs'; import Info from './Info'; import { IconBox } from '@tabler/icons'; import RequestsNotLoaded from './RequestsNotLoaded'; const Overview = ({ collection }) => { return (
    {collection?.name}
    ); }; export default Overview; ================================================ FILE: packages/bruno-app/src/components/CollectionSettings/Presets/StyledWrapper.js ================================================ import styled from 'styled-components'; const StyledWrapper = styled.div` max-width: 800px; .settings-label { width: 110px; } .textbox { border: 1px solid #ccc; padding: 0.15rem 0.45rem; box-shadow: none; border-radius: 0px; outline: none; box-shadow: none; transition: border-color ease-in-out 0.1s; border-radius: 3px; background-color: ${(props) => props.theme.input.bg}; border: 1px solid ${(props) => props.theme.input.border}; &:focus { border: solid 1px ${(props) => props.theme.input.focusBorder} !important; outline: none !important; } } `; export default StyledWrapper; ================================================ FILE: packages/bruno-app/src/components/CollectionSettings/Presets/index.js ================================================ import React from 'react'; import { useDispatch } from 'react-redux'; import StyledWrapper from './StyledWrapper'; import { updateCollectionPresets } from 'providers/ReduxStore/slices/collections'; import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions'; import { get } from 'lodash'; import Button from 'ui/Button'; const PresetsSettings = ({ collection }) => { const dispatch = useDispatch(); const initialPresets = { requestType: 'http', requestUrl: '' }; // Get presets from draft.brunoConfig if it exists, otherwise from brunoConfig const currentPresets = collection.draft?.brunoConfig ? get(collection, 'draft.brunoConfig.presets', initialPresets) : get(collection, 'brunoConfig.presets', initialPresets); // Helper to update presets config const updatePresets = (updates) => { const updatedPresets = { ...currentPresets, ...updates }; dispatch(updateCollectionPresets({ collectionUid: collection.uid, presets: updatedPresets })); }; const handleSave = () => dispatch(saveCollectionSettings(collection.uid)); const handleRequestTypeChange = (e) => { updatePresets({ requestType: e.target.value }); }; const handleRequestUrlChange = (e) => { updatePresets({ requestUrl: e.target.value }); }; return (
    These presets will be used as the default values for new requests in this collection.
    ); }; export default PresetsSettings; ================================================ FILE: packages/bruno-app/src/components/CollectionSettings/Protobuf/StyledWrapper.js ================================================ import styled from 'styled-components'; const StyledWrapper = styled.div` .available-certificates { background-color: ${(props) => props.theme.requestTabPanel.url.bg}; button.remove-certificate { color: ${(props) => props.theme.colors.text.danger}; } } /* Section labels */ label { color: ${(props) => props.theme.text}; } /* Tooltip icon */ .tooltip-icon { color: ${(props) => props.theme.colors.text.muted}; cursor: pointer; } /* Error messages */ .error-message { color: ${(props) => props.theme.colors.text.danger}; background-color: ${(props) => props.theme.bg}; border-radius: ${(props) => props.theme.border.radius.base}; } /* Tables */ table { width: 100%; border-collapse: collapse; thead { th { text-align: left; font-size: ${(props) => props.theme.font.size.xs}; font-weight: 500; text-transform: uppercase; letter-spacing: 0.05em; color: ${(props) => props.theme.table.thead.color}; border: 1px solid ${(props) => props.theme.table.border}; padding: 0.5rem 0.75rem; &.text-right { text-align: right; } } } tbody { td { border: 1px solid ${(props) => props.theme.table.border}; padding: 0.5rem 0.75rem; &.text-center { text-align: center; } &.text-right { text-align: right; } } } } /* File/Directory icons */ .file-icon, .folder-icon { color: ${(props) => props.theme.colors.text.muted}; } /* File/Directory names */ .file-name, .directory-name { font-weight: 500; color: ${(props) => props.theme.text}; } /* Path text */ .path-text { font-size: ${(props) => props.theme.font.size.xs}; color: ${(props) => props.theme.colors.text.muted}; font-family: monospace; } /* Empty state */ .empty-state { .empty-icon { color: ${(props) => props.theme.colors.text.muted}; } .empty-text { color: ${(props) => props.theme.colors.text.muted}; } } /* Invalid file indicator */ .invalid-indicator { color: ${(props) => props.theme.colors.text.danger}; } /* Action buttons */ .action-button { padding: 0.25rem; border-radius: ${(props) => props.theme.border.radius.base}; transition: all 0.2s; &.replace-button { color: ${(props) => props.theme.colors.text.danger}; &:hover { color: ${(props) => props.theme.colors.text.danger}; background-color: ${(props) => props.theme.colors.bg.danger}20; } } &.remove-button { color: ${(props) => props.theme.colors.text.muted}; &:hover { color: ${(props) => props.theme.text}; background-color: ${(props) => props.theme.dropdown.hoverBg}; } } } /* Checkbox */ input[type='checkbox'] { cursor: pointer; accent-color: ${(props) => props.theme.colors.accent}; border-color: ${(props) => props.theme.table.border}; &:focus { outline: none; border-color: ${(props) => props.theme.primary.solid}; } } /* Add button */ .btn-add-param { color: ${(props) => props.theme.textLink}; padding-right: 0.5rem; padding-top: 0.75rem; padding-bottom: 0.75rem; margin-top: 0.5rem; user-select: none; cursor: pointer; transition: color 0.2s; &:hover { color: ${(props) => props.theme.primary.solid}; } } `; export default StyledWrapper; ================================================ FILE: packages/bruno-app/src/components/CollectionSettings/Protobuf/index.js ================================================ import React, { useRef } from 'react'; import { useDispatch } from 'react-redux'; import StyledWrapper from './StyledWrapper'; import { IconTrash, IconFile, IconFileImport, IconAlertCircle, IconFolder } from '@tabler/icons'; import { getBasename } from 'utils/common/path'; import { Tooltip } from 'react-tooltip'; import useProtoFileManagement from '../../../hooks/useProtoFileManagement'; import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions'; import Button from 'ui/Button'; const ProtobufSettings = ({ collection }) => { const dispatch = useDispatch(); const { protoFiles, importPaths, addProtoFileToCollection, addImportPathToCollection, toggleImportPath, browseForProtoFile, browseForImportDirectory, removeProtoFileFromCollection, removeImportPathFromCollection, replaceImportPathInCollection, replaceProtoFileInCollection } = useProtoFileManagement(collection); const fileInputRef = useRef(null); const handleSave = () => dispatch(saveCollectionSettings(collection.uid)); // Get file path using the ipcRenderer const getProtoFile = async (event) => { const files = event?.files; if (files && files.length > 0) { for (let i = 0; i < files.length; i++) { const filePath = window?.ipcRenderer?.getFilePath(files[i]); if (filePath) { await addProtoFileToCollection(filePath); } } // Reset the file input if (fileInputRef.current) { fileInputRef.current.value = ''; } } }; const handleRemoveProtoFile = async (index) => { await removeProtoFileFromCollection(index); }; const handleBrowseClick = () => { if (fileInputRef.current) { fileInputRef.current.click(); } }; const handleReplaceProtoFile = async (index) => { const result = await browseForProtoFile(); if (result.success) { await replaceProtoFileInCollection(index, result.filePath); } }; const handleReplaceImportPath = async (index) => { const result = await browseForImportDirectory(); if (result.success) { await replaceImportPathInCollection(index, result.directoryPath); } }; const handleFileInputChange = (e) => { getProtoFile(e.target); }; const getImportPath = async () => { const result = await browseForImportDirectory(); if (result.success) { await addImportPathToCollection(result.directoryPath); } }; const handleRemoveImportPath = async (index) => { await removeImportPathFromCollection(index); }; const handleToggleImportPath = async (index) => { await toggleImportPath(index); }; const handleBrowseImportPathClick = () => { getImportPath(); }; return ( {/* Hidden file input for file selection */} {/* Proto Files Section */}
    {protoFiles.some((file) => !file.exists) && (
    Some proto files cannot be found. Use the replace option to update their locations.
    )} {protoFiles.length === 0 ? ( ) : ( protoFiles.map((file, index) => { const isValid = file.exists; return ( ); }) )}
    File Path Actions
    No proto files added
    {getBasename(collection.pathname, file.path)} {!isValid && }
    {file.path}
    {!isValid && ( )}
    {/* Import Paths Section */}
    {importPaths.some((path) => !path.exists) && (
    Some import paths cannot be found at their specified locations.
    )} {importPaths.length === 0 ? ( ) : ( importPaths.map((importPath, index) => { const isValid = importPath.exists; return ( ); }) )}
    Directory Path Actions
    No import paths added
    handleToggleImportPath(index)} className="h-4 w-4" title={importPath.enabled ? 'Disable this import path' : 'Enable this import path'} data-testid="protobuf-import-path-checkbox" />
    {getBasename(collection.pathname, importPath.path)} {!isValid && }
    {importPath.path}
    {!isValid && ( )}
    ); }; export default ProtobufSettings; ================================================ FILE: packages/bruno-app/src/components/CollectionSettings/ProxySettings/StyledWrapper.js ================================================ import styled from 'styled-components'; const StyledWrapper = styled.div` .settings-label { width: 80px; } .textbox { border: 1px solid #ccc; padding: 0.15rem 0.45rem; box-shadow: none; border-radius: 0px; outline: none; box-shadow: none; transition: border-color ease-in-out 0.1s; border-radius: 3px; background-color: ${(props) => props.theme.input.bg}; border: 1px solid ${(props) => props.theme.input.border}; &:focus { border: solid 1px ${(props) => props.theme.input.focusBorder} !important; outline: none !important; } } `; export default StyledWrapper; ================================================ FILE: packages/bruno-app/src/components/CollectionSettings/ProxySettings/index.js ================================================ import React from 'react'; import InfoTip from 'components/InfoTip'; import StyledWrapper from './StyledWrapper'; import { IconEye, IconEyeOff } from '@tabler/icons'; import { useState } from 'react'; import { useDispatch } from 'react-redux'; import { updateCollectionProxy } from 'providers/ReduxStore/slices/collections'; import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions'; import { get } from 'lodash'; import toast from 'react-hot-toast'; import Button from 'ui/Button'; const ProxySettings = ({ collection }) => { const dispatch = useDispatch(); const initialProxyConfig = { inherit: true, config: { protocol: 'http', hostname: '', port: '', auth: { username: '', password: '' }, bypassProxy: '' } }; // Get proxy from draft.brunoConfig if it exists, otherwise from brunoConfig const currentProxyConfig = collection.draft?.brunoConfig ? get(collection, 'draft.brunoConfig.proxy', initialProxyConfig) : get(collection, 'brunoConfig.proxy', initialProxyConfig); const [passwordVisible, setPasswordVisible] = useState(false); const validateHostnameOnChange = (hostname) => { if (hostname && hostname.length > 1024) { toast.error('Hostname must be less than 1024 characters'); return false; } return true; }; const validatePortOnChange = (port) => { if (!port || port === '') { return true; // Allow empty port during typing } const portNum = Number(port); if (isNaN(portNum)) { toast.error('Port must be a valid number'); return false; } if (portNum < 1 || portNum > 65535) { toast.error('Port must be between 1 and 65535'); return false; } return true; }; const validateAuthUsernameOnChange = (username) => { if (username && username.length > 1024) { toast.error('Username must be less than 1024 characters'); return false; } return true; }; const validateAuthPasswordOnChange = (password) => { if (password && password.length > 1024) { toast.error('Password must be less than 1024 characters'); return false; } return true; }; const validateBypassProxyOnChange = (bypassProxy) => { if (bypassProxy && bypassProxy.length > 1024) { toast.error('Bypass proxy must be less than 1024 characters'); return false; } return true; }; // Helper to update proxy config const updateProxy = (updates) => { const updatedProxy = { ...currentProxyConfig, ...updates }; dispatch(updateCollectionProxy({ collectionUid: collection.uid, proxy: updatedProxy })); }; const handleSave = () => dispatch(saveCollectionSettings(collection.uid)); const handleEnabledChange = (e) => { const value = e.target.value; // Map UI values to new format if (value === 'inherit') { updateProxy({ disabled: false, inherit: true }); } else if (value === 'true') { updateProxy({ disabled: false, inherit: false }); } else { updateProxy({ disabled: true, inherit: false }); } }; const handleProtocolChange = (e) => { updateProxy({ config: { ...currentProxyConfig.config, protocol: e.target.value } }); }; const handleHostnameChange = (e) => { const hostname = e.target.value; if (validateHostnameOnChange(hostname)) { updateProxy({ config: { ...currentProxyConfig.config, hostname } }); } }; const handlePortChange = (e) => { const port = e.target.value ? Number(e.target.value) : ''; if (validatePortOnChange(port)) { updateProxy({ config: { ...currentProxyConfig.config, port } }); } }; const handleAuthEnabledChange = (e) => { updateProxy({ config: { ...currentProxyConfig.config, auth: { ...currentProxyConfig.config.auth, disabled: !e.target.checked } } }); }; const handleAuthUsernameChange = (e) => { const username = e.target.value; if (validateAuthUsernameOnChange(username)) { updateProxy({ config: { ...currentProxyConfig.config, auth: { ...currentProxyConfig.config.auth, username } } }); } }; const handleAuthPasswordChange = (e) => { const password = e.target.value; if (validateAuthPasswordOnChange(password)) { updateProxy({ config: { ...currentProxyConfig.config, auth: { ...currentProxyConfig.config.auth, password } } }); } }; const handleBypassProxyChange = (e) => { const bypassProxy = e.target.value; if (validateBypassProxyOnChange(bypassProxy)) { updateProxy({ config: { ...currentProxyConfig.config, bypassProxy } }); } }; // Map new format to UI values const disabled = currentProxyConfig.disabled || false; const inherit = currentProxyConfig.inherit !== undefined ? currentProxyConfig.inherit : true; const enabledValue = disabled ? 'false' : (inherit ? 'inherit' : 'true'); return (
    Configure proxy settings for this collection.
    {enabledValue === 'true' && ( <>
    )}
    ); }; export default ProxySettings; ================================================ FILE: packages/bruno-app/src/components/CollectionSettings/Script/StyledWrapper.js ================================================ import styled from 'styled-components'; const StyledWrapper = styled.div` max-width: 800px; div.CodeMirror { height: inherit; } div.title { color: ${(props) => props.theme.colors.text.subtext0}; } `; export default StyledWrapper; ================================================ FILE: packages/bruno-app/src/components/CollectionSettings/Script/index.js ================================================ import React, { useState, useEffect, useRef } from 'react'; import get from 'lodash/get'; import { useDispatch, useSelector } from 'react-redux'; import CodeEditor from 'components/CodeEditor'; import { updateCollectionRequestScript, updateCollectionResponseScript } from 'providers/ReduxStore/slices/collections'; import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions'; import { useTheme } from 'providers/Theme'; import { Tabs, TabsList, TabsTrigger, TabsContent } from 'components/Tabs'; import StatusDot from 'components/StatusDot'; import { flattenItems, isItemARequest } from 'utils/collections'; import StyledWrapper from './StyledWrapper'; import Button from 'ui/Button'; const Script = ({ collection }) => { const dispatch = useDispatch(); const preRequestEditorRef = useRef(null); const postResponseEditorRef = useRef(null); const requestScript = collection.draft?.root ? get(collection, 'draft.root.request.script.req', '') : get(collection, 'root.request.script.req', ''); const responseScript = collection.draft?.root ? get(collection, 'draft.root.request.script.res', '') : get(collection, 'root.request.script.res', ''); // Default to post-response if pre-request script is empty const getInitialTab = () => { const hasPreRequestScript = requestScript && requestScript.trim().length > 0; return hasPreRequestScript ? 'pre-request' : 'post-response'; }; const [activeTab, setActiveTab] = useState(getInitialTab); const prevCollectionUidRef = useRef(collection.uid); const { displayedTheme } = useTheme(); const preferences = useSelector((state) => state.app.preferences); // Update active tab only when switching to a different collection useEffect(() => { if (prevCollectionUidRef.current !== collection.uid) { prevCollectionUidRef.current = collection.uid; const hasPreRequestScript = requestScript && requestScript.trim().length > 0; setActiveTab(hasPreRequestScript ? 'pre-request' : 'post-response'); } }, [collection.uid, requestScript]); // Refresh CodeMirror when tab becomes visible useEffect(() => { const timer = setTimeout(() => { if (activeTab === 'pre-request' && preRequestEditorRef.current?.editor) { preRequestEditorRef.current.editor.refresh(); } else if (activeTab === 'post-response' && postResponseEditorRef.current?.editor) { postResponseEditorRef.current.editor.refresh(); } }, 0); return () => clearTimeout(timer); }, [activeTab]); const onRequestScriptEdit = (value) => { dispatch( updateCollectionRequestScript({ script: value, collectionUid: collection.uid }) ); }; const onResponseScriptEdit = (value) => { dispatch( updateCollectionResponseScript({ script: value, collectionUid: collection.uid }) ); }; const handleSave = () => { dispatch(saveCollectionSettings(collection.uid)); }; const items = flattenItems(collection.items || []); const hasPreRequestScriptError = items.some((i) => isItemARequest(i) && i.preRequestScriptErrorMessage); const hasPostResponseScriptError = items.some((i) => isItemARequest(i) && i.postResponseScriptErrorMessage); return (
    Write pre and post-request scripts that will run before and after any request in this collection is sent.
    Pre Request {requestScript && requestScript.trim().length > 0 && ( )} Post Response {responseScript && responseScript.trim().length > 0 && ( )}
    ); }; export default Script; ================================================ FILE: packages/bruno-app/src/components/CollectionSettings/StyledWrapper.js ================================================ import styled from 'styled-components'; const StyledWrapper = styled.div` div.tabs { div.tab { padding: 6px 0px; border: none; border-bottom: solid 2px transparent; margin-right: ${(props) => props.theme.tabs.marginRight}; color: ${(props) => props.theme.colors.text.subtext0}; cursor: pointer; &:focus, &:active, &:focus-within, &:focus-visible, &:target { outline: none !important; box-shadow: none !important; } &:hover { color: ${(props) => props.theme.tabs.active.color} !important; } &.active { font-weight: ${(props) => props.theme.tabs.active.fontWeight} !important; color: ${(props) => props.theme.tabs.active.color} !important; border-bottom: solid 2px ${(props) => props.theme.tabs.active.border} !important; } } } table { thead, td { border: 1px solid ${(props) => props.theme.table.border}; li { background-color: ${(props) => props.theme.bg} !important; } } } .muted { color: ${(props) => props.theme.colors.text.muted}; } input[type='radio'] { cursor: pointer; accent-color: ${(props) => props.theme.primary.solid}; } `; export default StyledWrapper; ================================================ FILE: packages/bruno-app/src/components/CollectionSettings/Tests/StyledWrapper.js ================================================ import styled from 'styled-components'; const StyledWrapper = styled.div` max-width: 800px; `; export default StyledWrapper; ================================================ FILE: packages/bruno-app/src/components/CollectionSettings/Tests/index.js ================================================ import React from 'react'; import get from 'lodash/get'; import { useDispatch, useSelector } from 'react-redux'; import CodeEditor from 'components/CodeEditor'; import { updateCollectionTests } from 'providers/ReduxStore/slices/collections'; import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions'; import { useTheme } from 'providers/Theme'; import StyledWrapper from './StyledWrapper'; import Button from 'ui/Button'; const Tests = ({ collection }) => { const dispatch = useDispatch(); const tests = collection.draft?.root ? get(collection, 'draft.root.request.tests', '') : get(collection, 'root.request.tests', ''); const { displayedTheme } = useTheme(); const preferences = useSelector((state) => state.app.preferences); const onEdit = (value) => { dispatch( updateCollectionTests({ tests: value, collectionUid: collection.uid }) ); }; const handleSave = () => dispatch(saveCollectionSettings(collection.uid)); return (
    These tests will run any time a request in this collection is sent.
    ); }; export default Tests; ================================================ FILE: packages/bruno-app/src/components/CollectionSettings/Vars/StyledWrapper.js ================================================ import styled from 'styled-components'; const StyledWrapper = styled.div` max-width: 800px; div.title { color: ${(props) => props.theme.colors.text.subtext0}; } `; export default StyledWrapper; ================================================ FILE: packages/bruno-app/src/components/CollectionSettings/Vars/VarsTable/StyledWrapper.js ================================================ import styled from 'styled-components'; const Wrapper = styled.div` table { width: 100%; border-collapse: collapse; font-weight: 500; table-layout: fixed; thead, td { border: 1px solid ${(props) => props.theme.table.border}; } thead { color: ${(props) => props.theme.table.thead.color}; font-size: ${(props) => props.theme.font.size.base}; user-select: none; } td { padding: 6px 10px; &:nth-child(1) { width: 30%; } &:nth-child(3) { width: 70px; } } } .btn-add-var { font-size: ${(props) => props.theme.font.size.base}; } input[type='text'] { width: 100%; border: solid 1px transparent; outline: none !important; background-color: inherit; &:focus { outline: none !important; border: solid 1px transparent; } } input[type='checkbox'] { cursor: pointer; position: relative; top: 1px; } `; export default Wrapper; ================================================ FILE: packages/bruno-app/src/components/CollectionSettings/Vars/VarsTable/index.js ================================================ import React, { useCallback } from 'react'; import { useDispatch } from 'react-redux'; import { useTheme } from 'providers/Theme'; import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions'; import MultiLineEditor from 'components/MultiLineEditor'; import InfoTip from 'components/InfoTip'; import EditableTable from 'components/EditableTable'; import StyledWrapper from './StyledWrapper'; import toast from 'react-hot-toast'; import { variableNameRegex } from 'utils/common/regex'; import { setCollectionVars } from 'providers/ReduxStore/slices/collections/index'; const VarsTable = ({ collection, vars, varType }) => { const dispatch = useDispatch(); const { storedTheme } = useTheme(); const onSave = () => dispatch(saveCollectionSettings(collection.uid)); const handleVarsChange = useCallback((updatedVars) => { dispatch(setCollectionVars({ collectionUid: collection.uid, vars: updatedVars, type: varType })); }, [dispatch, collection.uid, varType]); const getRowError = useCallback((row, index, key) => { if (key !== 'name') return null; if (!row.name || row.name.trim() === '') return null; if (!variableNameRegex.test(row.name)) { return 'Variable contains invalid characters. Must only contain alphanumeric characters, "-", "_", "."'; } return null; }, []); const columns = [ { key: 'name', name: 'Name', isKeyField: true, placeholder: 'Name', width: '40%' }, { key: 'value', name: varType === 'request' ? 'Value' : (
    Expr
    ), placeholder: varType === 'request' ? 'Value' : 'Expr', render: ({ value, onChange }) => ( ) } ]; const defaultRow = { name: '', value: '', ...(varType === 'response' ? { local: false } : {}) }; return ( ); }; export default VarsTable; ================================================ FILE: packages/bruno-app/src/components/CollectionSettings/Vars/index.js ================================================ import React from 'react'; import get from 'lodash/get'; import VarsTable from './VarsTable'; import StyledWrapper from './StyledWrapper'; import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions'; import { useDispatch } from 'react-redux'; import Button from 'ui/Button'; const Vars = ({ collection }) => { const dispatch = useDispatch(); const requestVars = collection.draft?.root ? get(collection, 'draft.root.request.vars.req', []) : get(collection, 'root.request.vars.req', []); const responseVars = collection.draft?.root ? get(collection, 'draft.root.request.vars.res', []) : get(collection, 'root.request.vars.res', []); const handleSave = () => dispatch(saveCollectionSettings(collection.uid)); return (
    Pre Request
    Post Response
    ); }; export default Vars; ================================================ FILE: packages/bruno-app/src/components/CollectionSettings/index.js ================================================ import React from 'react'; import classnames from 'classnames'; import get from 'lodash/get'; import { updateSettingsSelectedTab } from 'providers/ReduxStore/slices/collections'; import { useDispatch } from 'react-redux'; import ProxySettings from './ProxySettings'; import ClientCertSettings from './ClientCertSettings'; import Headers from './Headers'; import Auth from './Auth'; import Script from './Script'; import Test from './Tests'; import Presets from './Presets'; import Protobuf from './Protobuf'; import StyledWrapper from './StyledWrapper'; import Vars from './Vars/index'; import StatusDot from 'components/StatusDot'; import Overview from './Overview/index'; const CollectionSettings = ({ collection }) => { const dispatch = useDispatch(); const tab = collection.settingsSelectedTab; const setTab = (tab) => { dispatch( updateSettingsSelectedTab({ collectionUid: collection.uid, tab }) ); }; const root = collection?.draft?.root || collection?.root; const hasScripts = root?.request?.script?.res || root?.request?.script?.req; const hasTests = root?.request?.tests; const hasDocs = root?.docs; const headers = collection.draft?.root ? get(collection, 'draft.root.request.headers', []) : get(collection, 'root.request.headers', []); const activeHeadersCount = headers.filter((header) => header.enabled).length; const requestVars = collection.draft?.root ? get(collection, 'draft.root.request.vars.req', []) : get(collection, 'root.request.vars.req', []); const responseVars = collection.draft?.root ? get(collection, 'draft.root.request.vars.res', []) : get(collection, 'root.request.vars.res', []); const activeVarsCount = requestVars.filter((v) => v.enabled).length + responseVars.filter((v) => v.enabled).length; const authMode = (collection.draft?.root ? get(collection, 'draft.root.request.auth', {}) : get(collection, 'root.request.auth', {})) .mode || 'none'; const proxyConfig = collection.draft?.brunoConfig ? get(collection, 'draft.brunoConfig.proxy', {}) : get(collection, 'brunoConfig.proxy', {}); const proxyEnabled = proxyConfig.hostname ? true : false; const clientCertConfig = collection.draft?.brunoConfig ? get(collection, 'draft.brunoConfig.clientCertificates.certs', []) : get(collection, 'brunoConfig.clientCertificates.certs', []); const protobufConfig = collection.draft?.brunoConfig ? get(collection, 'draft.brunoConfig.protobuf', {}) : get(collection, 'brunoConfig.protobuf', {}); const presets = collection.draft?.brunoConfig ? get(collection, 'draft.brunoConfig.presets', {}) : get(collection, 'brunoConfig.presets', {}); const hasPresets = presets && presets.requestUrl !== ''; const getTabPanel = (tab) => { switch (tab) { case 'overview': { return ; } case 'headers': { return ; } case 'vars': { return ; } case 'auth': { return ; } case 'script': { return
    `; const CollectionNotFound = ({ onClose }) => (
    Collection not found. It may have been deleted or is no longer available.
    ); const GenerateDocumentation = ({ onClose, collectionUid }) => { const { version } = useApp(); const collection = useSelector((state) => findCollectionByUid(state.collections.collections, collectionUid) ); const isLoading = useMemo( () => (collection ? areItemsLoading(collection) : false), [collection] ); const handleGenerate = useCallback(() => { try { const collectionCopy = cloneDeep(collection); const transformedCollection = transformCollectionToSaveToExportAsFile(collectionCopy); const openCollection = brunoToOpenCollection(transformedCollection); openCollection.extensions = { ...openCollection.extensions, bruno: { ...openCollection.extensions?.bruno, exportedAt: new Date().toISOString(), exportedUsing: version ? `Bruno/${version}` : 'Bruno' } }; const yamlContent = jsyaml.dump(openCollection, { indent: 2, lineWidth: -1, noRefs: true, sortKeys: false }); // jsesc handles all edge cases: Unicode, special chars, quotes, template literals, etc. let escapedYaml = jsesc(yamlContent, { quotes: 'double', wrap: true }); // Escape closing tags to prevent HTML parser from breaking out of the script block escapedYaml = escapedYaml.replace(/<\//g, '<\\/'); const htmlContent = buildHtmlDocument( escapeHtml(collection.name), escapedYaml ); const fileName = `${sanitizeName(collection.name)}-documentation.html`; FileSaver.saveAs(new Blob([htmlContent], { type: 'text/html' }), fileName); toast.success('Documentation generated successfully'); onClose(); } catch (error) { console.error('Error generating documentation:', error); toast.error('Failed to generate documentation'); } }, [collection, version, onClose]); if (!collection) { return ; } return ( {isLoading ? (
    Loading collection...
    ) : (

    Interactive API Documentation

    Generate a standalone HTML file that can be hosted anywhere or shared with your team.

    Sample Output Documentation preview
      {FEATURES.map((feature) => (
    • {feature}
    • ))}

    The generated file loads OpenCollection's JavaScript and CSS files from a CDN, which requires an internet connection.

    )}
    ); }; export default GenerateDocumentation; ================================================ FILE: packages/bruno-app/src/components/Sidebar/Collections/Collection/RemoveCollection/ConfirmCollectionCloseDrafts.js ================================================ import React, { useMemo } from 'react'; import filter from 'lodash/filter'; import { useDispatch, useSelector } from 'react-redux'; import { flattenItems, isItemARequest, hasRequestChanges, findCollectionByUid } from 'utils/collections'; import { pluralizeWord } from 'utils/common'; import { saveRequest, saveMultipleRequests } from 'providers/ReduxStore/slices/collections/actions'; import { deleteRequestDraft } from 'providers/ReduxStore/slices/collections'; import { removeCollection } from 'providers/ReduxStore/slices/collections/actions'; import { IconAlertTriangle, IconDeviceFloppy } from '@tabler/icons'; import Modal from 'components/Modal'; import toast from 'react-hot-toast'; import Button from 'ui/Button'; const MAX_UNSAVED_REQUESTS_TO_SHOW = 5; const ConfirmCollectionCloseDrafts = ({ onClose, collection, collectionUid }) => { const dispatch = useDispatch(); const latestCollection = useSelector((state) => findCollectionByUid(state.collections.collections, collectionUid)); const activeCollection = latestCollection || collection; const currentDrafts = useMemo(() => { if (!activeCollection) return []; const items = flattenItems(activeCollection.items); return items ?.filter((item) => isItemARequest(item) && hasRequestChanges(item) && !item.isTransient) .map((item) => { return { ...item, collectionUid: collectionUid }; }); }, [activeCollection, collectionUid]); const currentTransientDrafts = useMemo(() => { if (!activeCollection) return []; const items = flattenItems(activeCollection.items); return items ?.filter((item) => isItemARequest(item) && hasRequestChanges(item) && item.isTransient) .map((item) => { return { ...item, collectionUid: collectionUid }; }); }, [activeCollection, collectionUid]); const allDrafts = useMemo(() => { return [...currentDrafts, ...currentTransientDrafts]; }, [currentDrafts, currentTransientDrafts]); const handleSaveAll = () => { // If there are transient drafts, we can't proceed with batch save if (currentTransientDrafts.length > 0) { toast.error('Please save or discard transient requests first'); return; } // Save only non-transient drafts if (currentDrafts.length > 0) { dispatch(saveMultipleRequests(currentDrafts)) .then(() => { dispatch(removeCollection(collectionUid)) .then(() => { toast.success('Collection removed from workspace'); onClose(); }) .catch(() => toast.error('An error occurred while removing the collection')); }) .catch(() => { toast.error('Failed to save requests!'); }); } else { // No non-transient drafts, just remove the collection dispatch(removeCollection(collectionUid)) .then(() => { toast.success('Collection removed from workspace'); onClose(); }) .catch(() => toast.error('An error occurred while removing the collection')); } }; const handleDiscardAll = () => { // Discard all drafts (both regular and transient) allDrafts.forEach((draft) => { dispatch(deleteRequestDraft({ collectionUid: collectionUid, itemUid: draft.uid })); }); // Then remove the collection dispatch(removeCollection(collectionUid)) .then(() => { toast.success('Collection removed from workspace'); onClose(); }) .catch(() => toast.error('An error occurred while removing the collection')); }; const handleSaveTransient = (draft) => { dispatch(saveRequest(draft.uid, collectionUid)); }; if (!currentDrafts.length && !currentTransientDrafts.length) { return null; } return (

    Hold on..

    You have unsaved changes in {allDrafts.length}{' '} {pluralizeWord('request', allDrafts.length)}.

    {/* Regular (saved) requests with changes */} {currentDrafts.length > 0 && (

    Saved {pluralizeWord('Request', currentDrafts.length)} ({currentDrafts.length})

      {currentDrafts.slice(0, MAX_UNSAVED_REQUESTS_TO_SHOW).map((item) => { return (
    • • {item.filename || item.name}
    • ); })}
    {currentDrafts.length > MAX_UNSAVED_REQUESTS_TO_SHOW && (

    ...{currentDrafts.length - MAX_UNSAVED_REQUESTS_TO_SHOW} additional{' '} {pluralizeWord('request', currentDrafts.length - MAX_UNSAVED_REQUESTS_TO_SHOW)} not shown

    )}
    )} {/* Transient (unsaved) requests */} {currentTransientDrafts.length > 0 && (

    Transient {pluralizeWord('Request', currentTransientDrafts.length)} ({currentTransientDrafts.length})

    These requests need to be saved individually before closing the collection.

    {currentTransientDrafts.map((item) => { return (
    {item.name}
    ); })}
    )}
    ); }; export default ConfirmCollectionCloseDrafts; ================================================ FILE: packages/bruno-app/src/components/Sidebar/Collections/Collection/RemoveCollection/StyledWrapper.js ================================================ import styled from 'styled-components'; const StyledWrapper = styled.div` .collection-info-card { background-color: ${(props) => props.theme.modal.title.bg}; border-radius: 4px; padding: 12px; } .collection-name { font-weight: 500; padding-left: 0 !important; color: ${(props) => props.theme.text}; margin-bottom: 4px; cursor: default !important; &:hover { background: none !important; } } .collection-path { font-size: ${(props) => props.theme.font.size.sm}; color: ${(props) => props.theme.colors.text.muted}; word-break: break-all; } .warning-icon { color: ${(props) => props.theme.status.warning.text}; } `; export default StyledWrapper; ================================================ FILE: packages/bruno-app/src/components/Sidebar/Collections/Collection/RemoveCollection/index.js ================================================ import React, { useMemo } from 'react'; import toast from 'react-hot-toast'; import Modal from 'components/Modal'; import { useDispatch, useSelector } from 'react-redux'; import { IconAlertCircle } from '@tabler/icons'; import { removeCollection } from 'providers/ReduxStore/slices/collections/actions'; import { findCollectionByUid, flattenItems, isItemARequest, hasRequestChanges } from 'utils/collections/index'; import filter from 'lodash/filter'; import ConfirmCollectionCloseDrafts from './ConfirmCollectionCloseDrafts'; import StyledWrapper from './StyledWrapper'; const RemoveCollection = ({ onClose, collectionUid }) => { const dispatch = useDispatch(); const collection = useSelector((state) => findCollectionByUid(state.collections.collections, collectionUid)); // Detect drafts in the collection const drafts = useMemo(() => { if (!collection) return []; const items = flattenItems(collection.items); return filter(items, (item) => isItemARequest(item) && hasRequestChanges(item)); }, [collection]); const onConfirm = () => { if (!collection) { toast.error('Collection not found'); onClose(); return; } dispatch(removeCollection(collection.uid)) .then(() => { toast.success('Collection removed from workspace'); onClose(); }) .catch(() => toast.error('An error occurred while removing the collection')); }; if (!collection) { return
    Collection not found
    ; } // If there are drafts, show the draft confirmation modal if (drafts.length > 0) { return ; } const customHeader = (
    Remove Collection
    ); // Otherwise, show the standard remove confirmation modal return (

    Are you sure you want to close following collection in Bruno?

    {collection.name}
    {collection.pathname}

    It will still be available in the filesystem at the above location and can be re-opened later.

    ); }; export default RemoveCollection; ================================================ FILE: packages/bruno-app/src/components/Sidebar/Collections/Collection/RenameCollection/index.js ================================================ import React, { useRef, useEffect } from 'react'; import { useFormik } from 'formik'; import * as Yup from 'yup'; import Modal from 'components/Modal'; import { useDispatch, useSelector } from 'react-redux'; import toast from 'react-hot-toast'; import { renameCollection } from 'providers/ReduxStore/slices/collections/actions'; import { findCollectionByUid } from 'utils/collections/index'; const RenameCollection = ({ collectionUid, onClose }) => { const dispatch = useDispatch(); const inputRef = useRef(); const collection = useSelector((state) => findCollectionByUid(state.collections.collections, collectionUid)); const formik = useFormik({ enableReinitialize: true, initialValues: { name: collection.name }, validationSchema: Yup.object({ name: Yup.string() .min(1, 'must be at least 1 character') .required('name is required') }), onSubmit: (values) => { dispatch(renameCollection(values.name, collection.uid)) .then(() => { toast.success('Collection renamed!'); onClose(); }) .catch((err) => { toast.error(err ? err.message : 'An error occurred while renaming the collection'); }); } }); useEffect(() => { if (inputRef && inputRef.current) { inputRef.current.focus(); } }, [inputRef]); const onSubmit = () => formik.handleSubmit(); return (
    e.preventDefault()}>
    {formik.touched.name && formik.errors.name ?
    {formik.errors.name}
    : null}
    ); }; export default RenameCollection; ================================================ FILE: packages/bruno-app/src/components/Sidebar/Collections/Collection/StyledWrapper.js ================================================ import styled from 'styled-components'; const Wrapper = styled.div` .collection-name { height: 1.6rem; cursor: pointer; user-select: none; padding-left: 4px; border: ${(props) => props.theme.dragAndDrop.borderStyle} transparent; .rotate-90 { transform: rotateZ(90deg); } .collection-actions { visibility: hidden; } /* Single source of truth for hover/focus states: background and menu icon visibility */ &:hover, &:focus-within, &.collection-keyboard-focused { background: ${(props) => props.theme.sidebar.collection.item.hoverBg}; .collection-actions { visibility: visible; background-color: transparent !important; } } &.item-hovered { border-top: ${(props) => props.theme.dragAndDrop.borderStyle} ${(props) => props.theme.dragAndDrop.border}; border-bottom: 2px solid transparent; } &:hover { background: ${(props) => props.theme.sidebar.collection.item.hoverBg}; } div.tippy-box { position: relative; top: -0.625rem; font-weight: 400; } div.dropdown-item.delete-collection { color: ${(props) => props.theme.colors.text.danger}; &:hover { background-color: ${(props) => props.theme.colors.bg.danger}; color: white; } } &.drop-target { border: ${(props) => props.theme.dragAndDrop.borderStyle} ${(props) => props.theme.dragAndDrop.border}; background-color: ${(props) => props.theme.dragAndDrop.hoverBg}; transition: ${(props) => props.theme.dragAndDrop.transition}; } &.drop-target-above { border: none; border-top: ${(props) => props.theme.dragAndDrop.borderStyle} ${(props) => props.theme.dragAndDrop.border}; margin-top: -2px; background: transparent; transition: ${(props) => props.theme.dragAndDrop.transition}; } &.drop-target-below { border: none; border-bottom: ${(props) => props.theme.dragAndDrop.borderStyle} ${(props) => props.theme.dragAndDrop.border}; margin-bottom: -2px; background: transparent; transition: ${(props) => props.theme.dragAndDrop.transition}; } &.collection-focused-in-tab { background: ${(props) => props.theme.sidebar.collection.item.bg}; &:hover { background: ${(props) => props.theme.sidebar.collection.item.bg} !important; } } &.collection-keyboard-focused { border-top: 1px solid ${(props) => props.theme.sidebar.collection.item.focusBorder}; border-bottom: 1px solid ${(props) => props.theme.sidebar.collection.item.focusBorder}; outline: none; &:hover { background: ${(props) => props.theme.sidebar.collection.item.keyboardFocusBg} !important; } } } #sidebar-collection-name { white-space: nowrap; text-overflow: ellipsis; overflow: hidden; } .indent-block { border-right: 1px solid ${(props) => props.theme.sidebar.collection.item.indentBorder}; } .empty-collection-message { display: flex; align-items: center; height: 1.6rem; font-size: ${(props) => props.theme.font.size.sm}; color: ${(props) => props.theme.sidebar.muted}; .add-request-link { color: ${(props) => props.theme.textLink}; cursor: pointer; } } `; export default Wrapper; ================================================ FILE: packages/bruno-app/src/components/Sidebar/Collections/Collection/index.js ================================================ import React, { useState, useRef, useEffect } from 'react'; import { getEmptyImage } from 'react-dnd-html5-backend'; import classnames from 'classnames'; import { uuid } from 'utils/common'; import filter from 'lodash/filter'; import { useDrop, useDrag } from 'react-dnd'; import { IconChevronRight, IconDots, IconLoader2, IconFilePlus, IconFolderPlus, IconCopy, IconClipboard, IconPlayerPlay, IconEdit, IconShare, IconFoldDown, IconX, IconSettings, IconTerminal2, IconFolder, IconBook } from '@tabler/icons'; import OpenAPISyncIcon from 'components/Icons/OpenAPISync'; import { toggleCollection, collapseFullCollection } from 'providers/ReduxStore/slices/collections'; import { mountCollection, moveCollectionAndPersist, handleCollectionItemDrop, pasteItem, showInFolder, saveCollectionSecurityConfig } from 'providers/ReduxStore/slices/collections/actions'; import { useDispatch, useSelector } from 'react-redux'; import { addTab, makeTabPermanent } from 'providers/ReduxStore/slices/tabs'; import toast from 'react-hot-toast'; import NewRequest from 'components/Sidebar/NewRequest'; import NewFolder from 'components/Sidebar/NewFolder'; import CollectionItem from './CollectionItem'; import RemoveCollection from './RemoveCollection'; import { doesCollectionHaveItemsMatchingSearchText } from 'utils/collections/search'; import { isItemAFolder, isItemARequest, areItemsLoading } from 'utils/collections'; import { isTabForItemActive } from 'src/selectors/tab'; import RenameCollection from './RenameCollection'; import StyledWrapper from './StyledWrapper'; import CloneCollection from './CloneCollection'; import { scrollToTheActiveTab } from 'utils/tabs'; import ShareCollection from 'components/ShareCollection/index'; import GenerateDocumentation from './GenerateDocumentation'; import { CollectionItemDragPreview } from './CollectionItem/CollectionItemDragPreview/index'; import { sortByNameThenSequence } from 'utils/common/index'; import { getRevealInFolderLabel } from 'utils/common/platform'; import { openDevtoolsAndSwitchToTerminal } from 'utils/terminal'; import ActionIcon from 'ui/ActionIcon'; import MenuDropdown from 'ui/MenuDropdown'; import StatusBadge from 'ui/StatusBadge'; import { useBetaFeature, BETA_FEATURES } from 'utils/beta-features'; import { useSidebarAccordion } from 'components/Sidebar/SidebarAccordionContext'; import { createEmptyStateMenuItems } from 'utils/collections/emptyStateRequest'; // Delay before showing empty collection state (ms) // This prevents flicker from race condition between loading state and item batch updates const EMPTY_STATE_DELAY_MS = 300; const Collection = ({ collection, searchText }) => { const isOpenAPISyncEnabled = useBetaFeature(BETA_FEATURES.OPENAPI_SYNC); const { dropdownContainerRef } = useSidebarAccordion(); const [showNewFolderModal, setShowNewFolderModal] = useState(false); const [showNewRequestModal, setShowNewRequestModal] = useState(false); const [showRenameCollectionModal, setShowRenameCollectionModal] = useState(false); const [showCloneCollectionModalOpen, setShowCloneCollectionModalOpen] = useState(false); const [showShareCollectionModal, setShowShareCollectionModal] = useState(false); const [showGenerateDocumentationModal, setShowGenerateDocumentationModal] = useState(false); const [showRemoveCollectionModal, setShowRemoveCollectionModal] = useState(false); const [dropType, setDropType] = useState(null); const [isKeyboardFocused, setIsKeyboardFocused] = useState(false); const [showEmptyState, setShowEmptyState] = useState(false); const dispatch = useDispatch(); const isLoading = collection.isLoading; const collectionRef = useRef(null); // Only count persisted items; transients don't affect empty state const itemCount = collection.items?.filter((i) => !i.isTransient).length || 0; const isCollectionFocused = useSelector(isTabForItemActive({ itemUid: collection.uid })); const { hasCopiedItems } = useSelector((state) => state.app.clipboard); const menuDropdownRef = useRef(null); // Open the OpenAPI Sync tab const openOpenAPISyncTab = () => { ensureCollectionIsMounted(); dispatch( addTab({ uid: uuid(), collectionUid: collection.uid, type: 'openapi-sync' }) ); }; const handleRun = () => { dispatch( addTab({ uid: uuid(), collectionUid: collection.uid, type: 'collection-runner' }) ); }; const ensureCollectionIsMounted = () => { if (collection.mountStatus === 'mounted') { return; } dispatch(mountCollection({ collectionUid: collection.uid, collectionPathname: collection.pathname, brunoConfig: collection.brunoConfig })); }; const hasSearchText = searchText && searchText?.trim()?.length; const collectionIsCollapsed = hasSearchText ? false : collection.collapsed; const iconClassName = classnames({ 'rotate-90': !collectionIsCollapsed }); const handleClick = (event) => { if (event.detail != 1) return; // Check if the click came from the chevron icon const isChevronClick = event.target.closest('svg')?.classList.contains('chevron-icon'); setTimeout(scrollToTheActiveTab, 50); ensureCollectionIsMounted(); if (collection.collapsed) { dispatch(toggleCollection(collection.uid)); // Set default jsSandboxMode to 'safe' if not present and save to disk if (!collection.securityConfig?.jsSandboxMode) { dispatch(saveCollectionSecurityConfig(collection.uid, { jsSandboxMode: 'safe' })); } } if (!isChevronClick) { dispatch( addTab({ uid: collection.uid, collectionUid: collection.uid, type: 'collection-settings' }) ); } }; const handleDoubleClick = (_event) => { dispatch(makeTabPermanent({ uid: collection.uid })); }; const handleCollectionCollapse = (e) => { e.stopPropagation(); e.preventDefault(); ensureCollectionIsMounted(); dispatch(toggleCollection(collection.uid)); }; // prevent the parent's double-click handler from firing const handleCollectionDoubleClick = (e) => { e.stopPropagation(); e.preventDefault(); }; const handleRightClick = (event) => { event.preventDefault(); menuDropdownRef.current?.show(); }; const handleCollapseFullCollection = () => { dispatch(collapseFullCollection({ collectionUid: collection.uid })); }; const viewCollectionSettings = () => { dispatch( addTab({ uid: collection.uid, collectionUid: collection.uid, type: 'collection-settings' }) ); }; const handleShowInFolder = () => { dispatch(showInFolder(collection.pathname)).catch((error) => { console.error('Error opening the folder', error); toast.error('Error opening the folder'); }); }; const handlePasteItem = () => { dispatch(pasteItem(collection.uid, null)) .then(() => { toast.success('Item pasted successfully'); }) .catch((err) => { toast.error(err ? err.message : 'An error occurred while pasting the item'); }); }; // Keyboard shortcuts handler for collection const handleKeyDown = (e) => { // Detect Mac by checking both metaKey and platform const isMac = navigator.userAgent?.includes('Mac') || navigator.platform?.startsWith('Mac'); const isModifierPressed = isMac ? e.metaKey : e.ctrlKey; if (isModifierPressed && e.key.toLowerCase() === 'v') { e.preventDefault(); e.stopPropagation(); handlePasteItem(); } }; const handleFocus = () => { setIsKeyboardFocused(true); }; const handleBlur = () => { setIsKeyboardFocused(false); }; const isCollectionItem = (itemType) => { return itemType === 'collection-item'; }; const [{ isDragging }, drag, dragPreview] = useDrag({ type: 'collection', item: collection, collect: (monitor) => ({ isDragging: monitor.isDragging() }), options: { dropEffect: 'move' } }); const [{ isOver }, drop] = useDrop({ accept: ['collection', 'collection-item'], hover: (_draggedItem, monitor) => { const itemType = monitor.getItemType(); if (isCollectionItem(itemType)) { // For collection items, always show full highlight (inside drop) setDropType('inside'); } else { // For collections, show line indicator (adjacent drop) setDropType('adjacent'); } }, drop: (draggedItem, monitor) => { const itemType = monitor.getItemType(); if (isCollectionItem(itemType)) { dispatch(handleCollectionItemDrop({ targetItem: collection, draggedItem, dropType: 'inside', collectionUid: collection.uid })); } else { dispatch(moveCollectionAndPersist({ draggedItem, targetItem: collection })); } setDropType(null); }, canDrop: (draggedItem) => { return draggedItem.uid !== collection.uid; }, collect: (monitor) => ({ isOver: monitor.isOver() }) }); useEffect(() => { dragPreview(getEmptyImage(), { captureDraggingState: true }); }, []); useEffect(() => { if (isCollectionFocused && collectionRef.current) { try { collectionRef.current.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); } catch (err) { // ignore scroll errors } } }, [isCollectionFocused]); // Debounce showing empty state to prevent flicker // Race condition: isLoading can become false before items batch arrives from IPC useEffect(() => { const isMounted = collection.mountStatus === 'mounted'; const hasItems = itemCount > 0; if (hasItems || isLoading || !isMounted) { setShowEmptyState(false); return; } const timer = setTimeout(() => setShowEmptyState(true), EMPTY_STATE_DELAY_MS); return () => clearTimeout(timer); }, [itemCount, isLoading, collection.mountStatus]); if (searchText && searchText.length) { if (!doesCollectionHaveItemsMatchingSearchText(collection, searchText)) { return null; } } const collectionRowClassName = classnames('flex py-1 collection-name items-center', { 'item-hovered': isOver && dropType === 'adjacent', // For collection-to-collection moves (show line) 'drop-target': isOver && dropType === 'inside', // For collection-item drops (highlight full area) 'collection-focused-in-tab': isCollectionFocused && !isKeyboardFocused, 'collection-keyboard-focused': isKeyboardFocused }); // we need to sort request items by seq property const sortItemsBySequence = (items = []) => { return items.sort((a, b) => a.seq - b.seq); }; const requestItems = sortItemsBySequence(filter(collection.items, (i) => isItemARequest(i) && !i.isTransient)); const folderItems = sortByNameThenSequence(filter(collection.items, (i) => isItemAFolder(i) && !i.isTransient)); const showEmptyCollectionMessage = showEmptyState && !hasSearchText; const emptyStateMenuItems = createEmptyStateMenuItems({ dispatch, collection, itemUid: null }); const menuItems = [ { id: 'new-request', leftSection: IconFilePlus, label: 'New Request', onClick: () => { ensureCollectionIsMounted(); setShowNewRequestModal(true); } }, { id: 'new-folder', leftSection: IconFolderPlus, label: 'New Folder', onClick: () => { ensureCollectionIsMounted(); setShowNewFolderModal(true); } }, { id: 'run', leftSection: IconPlayerPlay, label: 'Run', onClick: () => { ensureCollectionIsMounted(); handleRun(); } }, { id: 'clone', leftSection: IconCopy, label: 'Clone', testId: 'clone-collection', onClick: () => { setShowCloneCollectionModalOpen(true); } }, ...(isOpenAPISyncEnabled ? [{ id: 'sync-openapi', leftSection: OpenAPISyncIcon, label: 'OpenAPI', rightSection: Beta, onClick: openOpenAPISyncTab }] : []), ...(hasCopiedItems ? [ { id: 'paste', leftSection: IconClipboard, label: 'Paste', onClick: handlePasteItem } ] : []), { id: 'rename', leftSection: IconEdit, label: 'Rename', onClick: () => { setShowRenameCollectionModal(true); } }, { id: 'share', leftSection: IconShare, label: 'Share', onClick: () => { ensureCollectionIsMounted(); setShowShareCollectionModal(true); } }, { id: 'generate-docs', leftSection: IconBook, label: 'Generate Docs', onClick: () => { ensureCollectionIsMounted(); setShowGenerateDocumentationModal(true); } }, { id: 'collapse', leftSection: IconFoldDown, label: 'Collapse', onClick: handleCollapseFullCollection }, { id: 'show-in-folder', leftSection: IconFolder, label: getRevealInFolderLabel(), onClick: handleShowInFolder }, { id: 'divider-1', type: 'divider' }, { id: 'settings', leftSection: IconSettings, label: 'Settings', onClick: viewCollectionSettings }, { id: 'terminal', leftSection: IconTerminal2, label: 'Open in Terminal', onClick: async () => { const collectionCwd = collection.pathname; await openDevtoolsAndSwitchToTerminal(dispatch, collectionCwd); } }, { id: 'remove', leftSection: IconX, label: 'Remove', onClick: () => { setShowRemoveCollectionModal(true); } } ]; return ( {showNewRequestModal && setShowNewRequestModal(false)} />} {showNewFolderModal && setShowNewFolderModal(false)} />} {showRenameCollectionModal && ( setShowRenameCollectionModal(false)} /> )} {showRemoveCollectionModal && ( setShowRemoveCollectionModal(false)} /> )} {showShareCollectionModal && ( setShowShareCollectionModal(false)} /> )} {showGenerateDocumentationModal && ( setShowGenerateDocumentationModal(false)} /> )} {showCloneCollectionModalOpen && ( setShowCloneCollectionModalOpen(false)} /> )}
    { collectionRef.current = node; drag(drop(node)); }} tabIndex={0} onKeyDown={handleKeyDown} onFocus={handleFocus} onBlur={handleBlur} data-testid="sidebar-collection-row" >
    {isLoading ? : null}
    {!collectionIsCollapsed ? (
    {folderItems?.map?.((i) => { return ; })} {requestItems?.map?.((i) => { return ; })} {showEmptyCollectionMessage ? (
     
    ) : null}
    ) : null}
    ); }; export default Collection; ================================================ FILE: packages/bruno-app/src/components/Sidebar/Collections/CollectionSearch/StyledWrapper.js ================================================ import styled from 'styled-components'; const StyledWrapper = styled.div` margin: 4px 10px 8px 10px; position: relative; .search-icon { position: absolute; left: 10px; top: 50%; transform: translateY(-50%); color: ${(props) => props.theme.sidebar.muted}; pointer-events: none; } input { width: 100%; height: 32px; padding: 0 32px 0 32px; font-size: 12px; color: ${(props) => props.theme.sidebar.color}; background: ${(props) => props.theme.sidebar.collection.item.hoverBg}; border: 1px solid transparent; border-radius: 6px; outline: none; transition: all 0.15s ease; &::placeholder { color: ${(props) => props.theme.sidebar.muted}; } &:hover { border-color: ${(props) => props.theme.input.border}; } &:focus { background: ${(props) => props.theme.input.bg}; border-color: ${(props) => props.theme.input.border}; } } .clear-icon { position: absolute; right: 8px; top: 50%; transform: translateY(-50%); display: flex; align-items: center; justify-content: center; width: 18px; height: 18px; border-radius: 4px; color: ${(props) => props.theme.sidebar.muted}; cursor: pointer; transition: all 0.15s ease; &:hover { color: ${(props) => props.theme.sidebar.color}; background: ${(props) => props.theme.sidebar.collection.item.hoverBg}; } } `; export default StyledWrapper; ================================================ FILE: packages/bruno-app/src/components/Sidebar/Collections/CollectionSearch/index.js ================================================ import { IconSearch, IconX } from '@tabler/icons'; import StyledWrapper from './StyledWrapper'; const CollectionSearch = ({ searchText, setSearchText }) => { return ( setSearchText(e.target.value.toLowerCase())} /> {searchText !== '' && (
    setSearchText('')}>
    )}
    ); }; export default CollectionSearch; ================================================ FILE: packages/bruno-app/src/components/Sidebar/Collections/CreateOrOpenCollection/StyledWrapper.js ================================================ import styled from 'styled-components'; const Wrapper = styled.div` color: ${(props) => props.theme.colors.text.muted}; `; export default Wrapper; ================================================ FILE: packages/bruno-app/src/components/Sidebar/Collections/CreateOrOpenCollection/index.js ================================================ import { useTheme } from '../../../../providers/Theme'; import { useDispatch } from 'react-redux'; import { openCollection } from 'providers/ReduxStore/slices/collections/actions'; import toast from 'react-hot-toast'; import styled from 'styled-components'; import StyledWrapper from './StyledWrapper'; const LinkStyle = styled.span` color: ${(props) => props.theme['text-link']}; `; const CreateOrOpenCollection = ({ onCreateClick }) => { const { theme } = useTheme(); const dispatch = useDispatch(); const handleOpenCollection = () => { dispatch(openCollection()).catch( (err) => { console.log(err); toast.error('An error occurred while opening the collection'); } ); }; const CreateLink = () => ( Create ); const OpenLink = () => ( handleOpenCollection(true)}> Open ); return (
    No collections found.
    or Collection.
    ); }; export default CreateOrOpenCollection; ================================================ FILE: packages/bruno-app/src/components/Sidebar/Collections/InlineCollectionCreator/StyledWrapper.js ================================================ import styled from 'styled-components'; const StyledWrapper = styled.div` .inline-collection-creator { display: flex; align-items: center; gap: 4px; height: 1.6rem; padding-left: 8px; padding-right: 4px; } .input-wrapper { display: flex; align-items: center; flex: 1; min-width: 0; border: 1px solid ${(props) => props.theme.input.border}; border-radius: 3px; background: ${(props) => props.theme.input.bg}; &:focus-within { border-color: ${(props) => props.theme.input.focusBorder}; } } .inline-collection-input { font-size: 13px; padding: 1px 4px; border: none; background: transparent; color: ${(props) => props.theme.text}; outline: none; flex: 1; min-width: 0; } .cog-btn { display: flex; align-items: center; justify-content: center; flex-shrink: 0; width: 20px; height: 100%; border: none; cursor: pointer; background: transparent; color: ${(props) => props.theme.text}; opacity: 0.5; &:hover { opacity: 1; } } .inline-actions { display: flex; align-items: center; gap: 2px; flex-shrink: 0; } .inline-action-btn { display: flex; align-items: center; justify-content: center; width: 20px; height: 20px; border: none; border-radius: 3px; cursor: pointer; background: transparent; color: ${(props) => props.theme.text}; &:hover { background: ${(props) => props.theme.sidebar.collection.item.hoverBg}; } &.save { color: ${(props) => props.theme.colors.text.green}; } &.cancel { color: ${(props) => props.theme.colors.text.danger}; } } `; export default StyledWrapper; ================================================ FILE: packages/bruno-app/src/components/Sidebar/Collections/InlineCollectionCreator/index.js ================================================ import { useRef, useEffect, useState, useCallback } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { IconCheck, IconX, IconSettings } from '@tabler/icons'; import get from 'lodash/get'; import path from 'utils/common/path'; import toast from 'react-hot-toast'; import { createCollection } from 'providers/ReduxStore/slices/collections/actions'; import { sanitizeName, validateName, validateNameError } from 'utils/common/regex'; import { DEFAULT_COLLECTION_FORMAT } from 'utils/common/constants'; import { multiLineMsg } from 'utils/common'; import { formatIpcError } from 'utils/common/error'; import StyledWrapper from './StyledWrapper'; const InlineCollectionCreator = ({ onComplete, onCancel, onOpenAdvanced }) => { const inputRef = useRef(null); const containerRef = useRef(null); const dispatch = useDispatch(); const [isCreating, setIsCreating] = useState(false); const openingAdvancedRef = useRef(false); const clickedOutsideRef = useRef(false); const preferences = useSelector((state) => state.app.preferences); const workspaces = useSelector((state) => state.workspaces?.workspaces || []); const activeWorkspaceUid = useSelector((state) => state.workspaces?.activeWorkspaceUid); const activeWorkspace = workspaces.find((w) => w.uid === activeWorkspaceUid); const isDefaultWorkspace = activeWorkspace?.type === 'default'; const defaultLocation = isDefaultWorkspace ? get(preferences, 'general.defaultLocation', '') : (activeWorkspace?.pathname ? path.join(activeWorkspace.pathname, 'collections') : ''); useEffect(() => { const focusAndSelect = (value) => { if (!inputRef.current) { return; } if (value) { inputRef.current.value = value; } inputRef.current.focus(); inputRef.current.select(); }; if (defaultLocation) { window.ipcRenderer?.invoke('renderer:find-unique-folder-name', 'Untitled Collection', defaultLocation) ?.then((name) => focusAndSelect(name)) ?.catch(() => focusAndSelect()); } else { focusAndSelect(); } }, [defaultLocation]); const handleCancel = () => { if (isCreating || openingAdvancedRef.current) return; onCancel(); }; const handleCreate = useCallback(async () => { const fromOutside = clickedOutsideRef.current; clickedOutsideRef.current = false; if (isCreating || openingAdvancedRef.current) return; const name = inputRef.current?.value?.trim(); if (!name) { if (fromOutside) { onCancel(); } else { toast.error('Collection name is required'); } return; } if (!validateName(name)) { toast.error(validateNameError(name)); if (fromOutside) { onCancel(); } return; } if (!defaultLocation) { toast.error('Please set a default location in Preferences > General'); onCancel(); return; } setIsCreating(true); try { const folderName = sanitizeName(name); await dispatch(createCollection(name, folderName, defaultLocation, { format: DEFAULT_COLLECTION_FORMAT })); toast.success('Collection created!'); onComplete(); } catch (e) { toast.error(multiLineMsg('An error occurred while creating the collection', formatIpcError(e))); setIsCreating(false); } }, [isCreating, defaultLocation, dispatch, onCancel, onComplete]); // Click outside to create useEffect(() => { const handleClickOutside = (e) => { if (containerRef.current && !containerRef.current.contains(e.target)) { clickedOutsideRef.current = true; handleCreate(); } }; document.addEventListener('mousedown', handleClickOutside); return () => document.removeEventListener('mousedown', handleClickOutside); }, [handleCreate]); const handleKeyDown = (e) => { if (e.key === 'Enter') { e.preventDefault(); handleCreate(); } else if (e.key === 'Escape') { e.preventDefault(); handleCancel(); } }; return (
    ); }; export default InlineCollectionCreator; ================================================ FILE: packages/bruno-app/src/components/Sidebar/Collections/RemoveCollectionsModal/StyledWrapper.js ================================================ import styled from 'styled-components'; const StyledWrapper = styled.div` width: 600px; overflow: hidden; box-sizing: border-box; .collections-list-container { width: 100%; max-height: 150px; overflow-y: auto; overflow-x: hidden; padding: 4px 0; box-sizing: border-box; } .collections-list { display: flex; flex-wrap: wrap; gap: 8px; width: 100%; } .collection-tag { display: inline-flex; align-items: center; padding: 6px 12px; background-color: ${(props) => props.theme.background.surface2}; border: 1px solid ${(props) => props.theme.requestTabs.bottomBorder}; border-radius: 4px; font-size: ${(props) => props.theme.font.size.sm}; font-weight: 500; color: ${(props) => props.theme.text}; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .collection-tag-text { display: inline-block; max-width: 100%; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .show-more-link, .show-less-link { display: inline-flex; align-items: center; &:hover { span { text-decoration: underline; } } } `; export default StyledWrapper; ================================================ FILE: packages/bruno-app/src/components/Sidebar/Collections/RemoveCollectionsModal/index.js ================================================ import React, { useState, useMemo } from 'react'; import toast from 'react-hot-toast'; import { useDispatch, useSelector } from 'react-redux'; import { filter, groupBy } from 'lodash'; import Modal from 'components/Modal'; import Portal from 'components/Portal'; import { removeCollection, saveMultipleRequests, saveMultipleCollections, saveMultipleFolders } from 'providers/ReduxStore/slices/collections/actions'; import { findCollectionByUid, flattenItems, isItemARequest, isItemAFolder, hasRequestChanges } from 'utils/collections/index'; import { IconAlertTriangle } from '@tabler/icons'; import StyledWrapper from './StyledWrapper'; import Button from 'ui/Button'; const MAX_COLLECTIONS_WIDTH = 530; const CHARACTER_WIDTH = 8; const COLLECTION_PADDING = 24; const COLLECTION_GAP = 12; const getDisplayItems = (items, maxWidth = MAX_COLLECTIONS_WIDTH) => { const visibleItems = []; let totalWidth = 0; for (let i = 0; i < items.length; i += 1) { const currentItem = items[i]; const name = typeof currentItem === 'string' ? currentItem : currentItem?.name || ''; const width = name.length * CHARACTER_WIDTH + COLLECTION_PADDING + COLLECTION_GAP; if (i === 0 || totalWidth + width <= maxWidth) { totalWidth += width; visibleItems.push(currentItem); } else { break; } } return visibleItems; }; const RemoveCollectionsModal = ({ collectionUids, onClose }) => { const dispatch = useDispatch(); const allCollections = useSelector((state) => state.collections.collections || []); const [showAllCollections, setShowAllCollections] = useState(false); const allDrafts = useMemo(() => { const requestDrafts = []; const collectionDrafts = []; const folderDrafts = []; collectionUids.forEach((collectionUid) => { const collection = findCollectionByUid(allCollections, collectionUid); if (!collection) { return; } // Check for collection draft if (collection.draft) { collectionDrafts.push({ name: collection.name, collectionUid: collectionUid }); } // Check for request and folder drafts const items = flattenItems(collection.items); // Request drafts const unsavedRequests = filter(items, (item) => isItemARequest(item) && hasRequestChanges(item)); unsavedRequests.forEach((request) => { requestDrafts.push({ ...request, collectionUid: collectionUid }); }); // Folder drafts const unsavedFolders = filter(items, (item) => isItemAFolder(item) && item.draft); unsavedFolders.forEach((folder) => { folderDrafts.push({ name: folder.name, folderUid: folder.uid, collectionUid: collectionUid }); }); }); return { requestDrafts, collectionDrafts, folderDrafts }; }, [collectionUids, allCollections]); const collectionsWithUnsavedChanges = useMemo(() => { const allDraftTypes = [...allDrafts.collectionDrafts, ...allDrafts.folderDrafts, ...allDrafts.requestDrafts]; const draftsByCollection = groupBy(allDraftTypes, 'collectionUid'); return Object.keys(draftsByCollection) .map((collectionUid) => { const collection = findCollectionByUid(allCollections, collectionUid); return collection ? { uid: collectionUid, name: collection.name } : null; }) .filter(Boolean); }, [allDrafts, allCollections]); const hasUnsavedChanges = allDrafts.collectionDrafts.length > 0 || allDrafts.folderDrafts.length > 0 || allDrafts.requestDrafts.length > 0; const handleCloseAllCollections = () => { const removalPromises = collectionUids.map((uid) => dispatch(removeCollection(uid))); Promise.all(removalPromises) .then(() => { toast.success('Closed all collections'); }) .catch((error) => { console.error('Error closing collections:', error); toast.error('An error occurred while closing collections'); }) .finally(() => { onClose(); }); }; const handleDiscard = () => { handleCloseAllCollections(); }; const handleCancel = () => { onClose(); }; const handleSave = async () => { try { const savePromises = []; // Save all collection drafts if (allDrafts.collectionDrafts.length > 0) { savePromises.push(dispatch(saveMultipleCollections(allDrafts.collectionDrafts))); } // Save all folder drafts if (allDrafts.folderDrafts.length > 0) { savePromises.push(dispatch(saveMultipleFolders(allDrafts.folderDrafts))); } // Save all request drafts if (allDrafts.requestDrafts.length > 0) { savePromises.push(dispatch(saveMultipleRequests(allDrafts.requestDrafts))); } await Promise.all(savePromises); handleCloseAllCollections(); } catch (error) { console.error('Error saving drafts:', error); toast.error('An error occurred while saving changes'); handleCancel(); } }; if (collectionUids.length === 0) { return null; } const hasMultipleCollections = collectionUids.length > 1; const singleCollectionName = hasMultipleCollections ? null : findCollectionByUid(allCollections, collectionUids[0])?.name; const displayedCollections = useMemo(() => showAllCollections ? collectionsWithUnsavedChanges : getDisplayItems(collectionsWithUnsavedChanges), [collectionsWithUnsavedChanges, showAllCollections]); const hasMoreCollections = collectionsWithUnsavedChanges.length > displayedCollections.length; const hiddenCollectionsCount = collectionsWithUnsavedChanges.length - displayedCollections.length; const toggleButton = hasMoreCollections || showAllCollections ? ( setShowAllCollections(!showAllCollections)} > {showAllCollections ? 'Show less' : `Show ${hiddenCollectionsCount} more`} ) : null; return ( {hasUnsavedChanges ? ( <>

    Hold on..

    Do you want to save changes you made to the following{' '} {collectionsWithUnsavedChanges.length === 1 ? 'collection' : 'collections'}?
    Collections will be removed from the current workspace but will still be available in the file system and can be re-opened later.
    {displayedCollections.map(({ uid, name }) => ( {name} ))} {toggleButton}
    ) : ( <>
    {hasMultipleCollections ? ( `Are you sure you want to close all ${collectionUids.length} collections in this workspace?` ) : ( <> Are you sure you want to close the collection {singleCollectionName} from this workspace? )}
    Collections will be removed from the current workspace but will still be available in the file system and can be re-opened later.
    )}
    ); }; export default RemoveCollectionsModal; ================================================ FILE: packages/bruno-app/src/components/Sidebar/Collections/SelectCollection/StyledWrapper.js ================================================ import styled from 'styled-components'; const StyledWrapper = styled.div` div.collection { padding: 4px 6px; padding-left: 8px; display: flex; align-items: center; border-radius: 3px; cursor: pointer; &:hover { background-color: ${(props) => props.theme.plainGrid.hoverBg}; } } `; export default StyledWrapper; ================================================ FILE: packages/bruno-app/src/components/Sidebar/Collections/SelectCollection/index.js ================================================ import React from 'react'; import Modal from 'components/Modal/index'; import { IconFiles } from '@tabler/icons'; import { useSelector } from 'react-redux'; import StyledWrapper from './StyledWrapper'; const SelectCollection = ({ onClose, onSelect, title }) => { const { collections } = useSelector((state) => state.collections); return (
      {collections && collections.length ? ( collections.map((c) => (
      onSelect(c.uid)}> {c.name}
      )) ) : (
      No collections found
      )}
    ); }; export default SelectCollection; ================================================ FILE: packages/bruno-app/src/components/Sidebar/Collections/StyledWrapper.js ================================================ import styled from 'styled-components'; const Wrapper = styled.div` display: flex; flex-direction: column; flex: 1 1 0%; min-height: 0; overflow: hidden; padding-top: 4px; padding-bottom: 4px; .collections-list { flex: 1 1 0%; min-height: 0; padding-top: 4px; padding-bottom: 4px; overflow-y: auto; overflow-x: hidden; } `; export default Wrapper; ================================================ FILE: packages/bruno-app/src/components/Sidebar/Collections/index.js ================================================ import React, { useState, useMemo } from 'react'; import { useSelector } from 'react-redux'; import Collection from './Collection'; import StyledWrapper from './StyledWrapper'; import CreateOrOpenCollection from './CreateOrOpenCollection'; import CollectionSearch from './CollectionSearch/index'; import InlineCollectionCreator from './InlineCollectionCreator'; import { normalizePath } from 'utils/common/path'; import { isScratchCollection } from 'utils/collections'; const Collections = ({ showSearch, isCreatingCollection, onCreateClick, onDismissCreate, onOpenAdvancedCreate }) => { const [searchText, setSearchText] = useState(''); const { collections } = useSelector((state) => state.collections); const { workspaces, activeWorkspaceUid } = useSelector((state) => state.workspaces); const activeWorkspace = workspaces.find((w) => w.uid === activeWorkspaceUid) || workspaces.find((w) => w.type === 'default'); const workspaceCollections = useMemo(() => { if (!activeWorkspace) return []; return collections.filter((c) => { if (isScratchCollection(c, workspaces)) { return false; } return activeWorkspace.collections?.some((wc) => normalizePath(wc.path) === normalizePath(c.pathname)); }); }, [activeWorkspace, collections, workspaces]); if (!workspaceCollections || !workspaceCollections.length) { return ( {isCreatingCollection && ( )} {!isCreatingCollection && } ); } return ( {showSearch && ( )}
    {isCreatingCollection && ( )} {workspaceCollections && workspaceCollections.length ? workspaceCollections.map((c) => { return ( ); }) : null}
    ); }; export default Collections; ================================================ FILE: packages/bruno-app/src/components/Sidebar/CreateCollection/StyledWrapper.js ================================================ import styled from 'styled-components'; const StyledWrapper = styled.div` .advanced-options { .caret { color: ${(props) => props.theme.textLink}; fill: ${(props) => props.theme.textLink}; } } .report-issue-link { display: inline-flex; align-items: center; gap: 0.375rem; font-size: ${(props) => props.theme.font.size.sm}; color: ${(props) => props.theme.textLink}; cursor: pointer; transition: opacity 0.15s ease; &:hover { opacity: 0.8; text-decoration: underline; } svg { flex-shrink: 0; } } `; export default StyledWrapper; ================================================ FILE: packages/bruno-app/src/components/Sidebar/CreateCollection/index.js ================================================ import React, { useRef, useEffect, useState, forwardRef } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { useFormik } from 'formik'; import * as Yup from 'yup'; import path from 'utils/common/path'; import { browseDirectory, createCollection } from 'providers/ReduxStore/slices/collections/actions'; import toast from 'react-hot-toast'; import Portal from 'components/Portal'; import Modal from 'components/Modal'; import { sanitizeName, validateName, validateNameError } from 'utils/common/regex'; import PathDisplay from 'components/PathDisplay/index'; import { IconArrowBackUp, IconEdit, IconCaretDown } from '@tabler/icons'; import Help from 'components/Help'; import Dropdown from 'components/Dropdown'; import { multiLineMsg } from 'utils/common'; import { formatIpcError } from 'utils/common/error'; import { DEFAULT_COLLECTION_FORMAT } from 'utils/common/constants'; import StyledWrapper from './StyledWrapper'; import get from 'lodash/get'; import Button from 'ui/Button'; const CreateCollection = ({ onClose, defaultLocation: propDefaultLocation, initialCollectionName = '' }) => { const inputRef = useRef(); const dispatch = useDispatch(); const workspaces = useSelector((state) => state.workspaces?.workspaces || []); const workspaceUid = useSelector((state) => state.workspaces?.activeWorkspaceUid); const [isEditing, toggleEditing] = useState(false); const [showFileFormat, setShowFileFormat] = useState(false); const preferences = useSelector((state) => state.app.preferences); const dropdownTippyRef = useRef(); const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref); const activeWorkspace = workspaces.find((w) => w.uid === workspaceUid); const isDefaultWorkspace = activeWorkspace?.type === 'default'; const defaultLocation = isDefaultWorkspace ? get(preferences, 'general.defaultLocation', '') : (activeWorkspace?.pathname ? path.join(activeWorkspace.pathname, 'collections') : ''); const formik = useFormik({ enableReinitialize: true, initialValues: { collectionName: initialCollectionName, collectionFolderName: initialCollectionName ? sanitizeName(initialCollectionName) : '', collectionLocation: defaultLocation || '', format: DEFAULT_COLLECTION_FORMAT }, validationSchema: Yup.object({ collectionName: Yup.string() .min(1, 'must be at least 1 character') .max(255, 'must be 255 characters or less') .required('collection name is required'), collectionFolderName: Yup.string() .min(1, 'must be at least 1 character') .max(255, 'must be 255 characters or less') .test('is-valid-collection-name', function (value) { const isValid = validateName(value); return isValid ? true : this.createError({ message: validateNameError(value) }); }) .required('folder name is required'), collectionLocation: Yup.string().min(1, 'location is required').required('location is required'), format: Yup.string().oneOf(['bru', 'yml'], 'invalid format').required('format is required') }), onSubmit: async (values) => { try { await dispatch(createCollection(values.collectionName, values.collectionFolderName, values.collectionLocation, { format: values.format })); toast.success('Collection created!'); onClose(); } catch (e) { toast.error(multiLineMsg('An error occurred while creating the collection', formatIpcError(e))); } } }); const browse = () => { dispatch(browseDirectory()) .then((dirPath) => { if (typeof dirPath === 'string') { formik.setFieldValue('collectionLocation', dirPath); } }) .catch(() => { formik.setFieldValue('collectionLocation', ''); }); }; useEffect(() => { const timer = setTimeout(() => { if (inputRef && inputRef.current) { inputRef.current.focus(); inputRef.current.select(); } }, 50); return () => clearTimeout(timer); }, [inputRef]); const AdvancedOptions = forwardRef((props, ref) => { return (
    ); }); return (
    { formik.handleChange(e); !isEditing && formik.setFieldValue('collectionFolderName', sanitizeName(e.target.value)); }} autoComplete="off" autoCorrect="off" autoCapitalize="off" spellCheck="false" value={formik.values.collectionName || ''} /> {formik.touched.collectionName && formik.errors.collectionName ? (
    {formik.errors.collectionName}
    ) : null} { formik.setFieldValue('collectionLocation', e.target.value); }} /> {formik.touched.collectionLocation && formik.errors.collectionLocation ? (
    {formik.errors.collectionLocation}
    ) : null}
    Browse
    {formik.values.collectionName?.trim()?.length > 0 && (
    {isEditing ? ( toggleEditing(false)} /> ) : ( toggleEditing(true)} /> )}
    {isEditing ? ( ) : (
    )} {formik.touched.collectionFolderName && formik.errors.collectionFolderName ? (
    {formik.errors.collectionFolderName}
    ) : null}
    )} {showFileFormat && (
    {formik.touched.format && formik.errors.format ? (
    {formik.errors.format}
    ) : null}
    )}
    } placement="bottom-start">
    { dropdownTippyRef.current.hide(); setShowFileFormat(!showFileFormat); }} > {showFileFormat ? 'Hide File Format' : 'Show File Format'}
    ); }; export default CreateCollection; ================================================ FILE: packages/bruno-app/src/components/Sidebar/GoldenEdition/StyledWrapper.js ================================================ import styled from 'styled-components'; const StyledWrapper = styled.div` color: ${(props) => props.theme.text}; .collection-options { svg { position: relative; top: -1px; } .label { cursor: pointer; &:hover { text-decoration: underline; } } } `; export default StyledWrapper; ================================================ FILE: packages/bruno-app/src/components/Sidebar/GoldenEdition/index.js ================================================ import React, { useState, useEffect } from 'react'; import Modal from 'components/Modal/index'; import { PostHog } from 'posthog-node'; import { uuid } from 'utils/common'; import { IconHeart, IconUser, IconUsers, IconPlus } from '@tabler/icons'; import platformLib from 'platform'; import StyledWrapper from './StyledWrapper'; import { useTheme } from 'providers/Theme/index'; let posthogClient = null; const posthogApiKey = process.env.NEXT_PUBLIC_POSTHOG_API_KEY; const getPosthogClient = () => { if (posthogClient) { return posthogClient; } posthogClient = new PostHog(posthogApiKey); return posthogClient; }; const getAnonymousTrackingId = () => { let id = localStorage.getItem('bruno.anonymousTrackingId'); if (!id || !id.length || id.length !== 21) { id = uuid(); localStorage.setItem('bruno.anonymousTrackingId', id); } return id; }; const HeartIcon = () => { return ( ); }; const CheckIcon = () => { return ( ); }; const GoldenEdition = ({ onClose }) => { const { displayedTheme } = useTheme(); useEffect(() => { const anonymousId = getAnonymousTrackingId(); const client = getPosthogClient(); client.capture({ distinctId: anonymousId, event: 'golden-edition-modal-opened', properties: { os: platformLib.os.family } }); }, []); const goldenEditionBuyClick = () => { const anonymousId = getAnonymousTrackingId(); const client = getPosthogClient(); client.capture({ distinctId: anonymousId, event: 'golden-edition-buy-clicked', properties: { os: platformLib.os.family } }); }; const goldenEditionIndividuals = [ 'Inbuilt Bru File Explorer', 'Visual Git (Like Gitlens for Vscode)', 'GRPC, Websocket, SocketIO, MQTT', 'Load Data from File for Collection Run', 'Developer Tools', 'OpenAPI Designer', 'Performance/Load Testing', 'Inbuilt Terminal', 'Custom Themes' ]; const goldenEditionOrganizations = [ 'Centralized License Management', 'Integration with Secret Managers', 'Private Collection Registry', 'Request Forms', 'Priority Support' ]; const [pricingOption, setPricingOption] = useState('individuals'); const handlePricingOptionChange = (option) => { setPricingOption(option); }; const themeBasedContainerClassNames = displayedTheme === 'light' ? 'text-gray-900' : 'text-white'; const themeBasedTabContainerClassNames = displayedTheme === 'light' ? 'bg-gray-200' : 'bg-gray-800'; const themeBasedActiveTabClassNames = displayedTheme === 'light' ? 'bg-white text-gray-900 font-medium' : 'bg-gray-700 text-white font-medium'; return (
    {pricingOption === 'individuals' ? (
    $19

    One Time Payment

    perpetual license for 2 devices, with 2 years of updates

    ) : (
    $49 / user

    One Time Payment

    perpetual license with 2 years of updates

    )}
    handlePricingOptionChange('individuals')} > Individuals
    handlePricingOptionChange('organizations')} > Organizations
    • Support Bruno's Development
    • {pricingOption === 'individuals' ? ( <> {goldenEditionIndividuals.map((item, index) => (
    • {item}
    • ))} ) : ( <>
    • Everything in the Individual Plan
    • {goldenEditionOrganizations.map((item, index) => (
    • {item}
    • ))} )}
    ); }; export default GoldenEdition; ================================================ FILE: packages/bruno-app/src/components/Sidebar/ImportCollection/FileTab.js ================================================ import React, { useState, useRef } from 'react'; import { IconFileImport } from '@tabler/icons'; import { toastError } from 'utils/common/error'; import jsyaml from 'js-yaml'; import { isPostmanCollection } from 'utils/importers/postman-collection'; import { isInsomniaCollection } from 'utils/importers/insomnia-collection'; import { isOpenApiSpec } from 'utils/importers/openapi-collection'; import { isWSDLCollection } from 'utils/importers/wsdl-collection'; import { isBrunoCollection } from 'utils/importers/bruno-collection'; import { isOpenCollection } from 'utils/importers/opencollection'; import { useTheme } from 'providers/Theme'; const convertFileToObject = async (file) => { const text = await file.text(); // Handle WSDL files - return as plain text if (file.name.endsWith('.wsdl') || file.type === 'text/xml' || file.type === 'application/xml') { return text; } try { if (file.type === 'application/json' || file.name.endsWith('.json')) { return JSON.parse(text); } const parsed = jsyaml.load(text); if (typeof parsed !== 'object' || parsed === null) { throw new Error(); } return parsed; } catch { throw new Error('Failed to parse the file – ensure it is valid JSON or YAML'); } }; const FileTab = ({ setIsLoading, handleSubmit, setErrorMessage }) => { const [dragActive, setDragActive] = useState(false); const fileInputRef = useRef(null); const { theme } = useTheme(); const acceptedFileTypes = [ '.json', '.yaml', '.yml', '.wsdl', '.zip', 'application/json', 'application/yaml', 'application/x-yaml', 'application/zip', 'application/x-zip-compressed', 'text/xml', 'application/xml' ]; const handleDrag = (e) => { e.preventDefault(); e.stopPropagation(); if (e.dataTransfer) { e.dataTransfer.dropEffect = 'copy'; } if (e.type === 'dragenter' || e.type === 'dragover') { setDragActive(true); } else if (e.type === 'dragleave') { setDragActive(false); } }; const processZipFile = async (zipFile) => { setIsLoading(true); try { const filePath = window.ipcRenderer.getFilePath(zipFile); const isBrunoZip = await window.ipcRenderer.invoke('renderer:is-bruno-collection-zip', filePath); if (isBrunoZip) { const collectionName = zipFile.name.replace(/\.zip$/i, ''); await handleSubmit({ rawData: { zipFilePath: filePath, collectionName }, type: 'bruno-zip' }); return; } toastError(new Error('The ZIP file is not a valid Bruno collection')); } catch (err) { toastError(err, 'Import ZIP file failed'); } finally { setIsLoading(false); } }; const handleMultipleFiles = async (fileArray) => { setIsLoading(true); try { const filesData = []; // Parse all files for (const file of fileArray) { try { const data = await convertFileToObject(file); // Determine type for each file let type = null; if (isOpenApiSpec(data)) { type = 'openapi'; } else if (isWSDLCollection(data)) { type = 'wsdl'; } else if (isPostmanCollection(data)) { type = 'postman'; } else if (isInsomniaCollection(data)) { type = 'insomnia'; } else if (isOpenCollection(data)) { type = 'opencollection'; } else if (isBrunoCollection(data)) { type = 'bruno'; } if (type) { filesData.push({ file, data, type }); } } catch (err) { console.warn(`Failed to process file ${file.name}:`, err); } } if (filesData.length > 0) { // Pass raw filesData to be processed in BulkImportCollectionLocation handleSubmit({ filesData, type: 'multiple' }); } else { throw new Error('No valid collections found in the selected files'); } } catch (err) { toastError(err, 'Import multiple files failed'); } finally { setIsLoading(false); } }; const processFile = async (file) => { setIsLoading(true); try { const data = await convertFileToObject(file); if (!data) { throw new Error('Failed to parse file content'); } let type = null; if (isOpenApiSpec(data)) { type = 'openapi'; } else if (isWSDLCollection(data)) { type = 'wsdl'; } else if (isPostmanCollection(data)) { type = 'postman'; } else if (isInsomniaCollection(data)) { type = 'insomnia'; } else if (isOpenCollection(data)) { type = 'opencollection'; } else if (isBrunoCollection(data)) { type = 'bruno'; } else { throw new Error('Unsupported collection format'); } if (type === 'openapi') { const filePath = window.ipcRenderer.getFilePath(file); const rawContent = await file.text(); await handleSubmit({ rawData: data, type, filePath, rawContent }); } else { await handleSubmit({ rawData: data, type }); } } catch (err) { toastError(err, 'Import collection failed'); } finally { setIsLoading(false); } }; const processFiles = async (files) => { setErrorMessage(''); const fileArray = Array.from(files); const zipFiles = fileArray.filter((file) => file.name.endsWith('.zip')); // If both ZIP and non-ZIP files are selected, show error if (zipFiles.length && (fileArray.length - zipFiles.length > 0)) { setErrorMessage('Cannot mix ZIP files with other file types. Please select either a single ZIP file OR collection files (JSON/YAML)'); return; } if (zipFiles.length > 1) { setErrorMessage('Multiple ZIP files selected. Please select only one ZIP file at a time for import.'); return; } if (zipFiles.length) { await processZipFile(zipFiles[0]); return; } if (fileArray.length > 1) { // Process multiple non-ZIP files normally await handleMultipleFiles(fileArray); } else if (fileArray.length === 1) { await processFile(fileArray[0]); } }; const handleDrop = async (e) => { e.preventDefault(); e.stopPropagation(); setDragActive(false); if (e.dataTransfer.files && e.dataTransfer.files.length > 0) { await processFiles(e.dataTransfer.files); } }; const handleBrowseFiles = () => { setErrorMessage(''); fileInputRef.current.click(); }; const handleFileInputChange = async (e) => { if (e.target.files && e.target.files.length > 0) { await processFiles(e.target.files); e.target.value = ''; } }; return (

    Drop file(s) to import or{' '}

    Supports Bruno, OpenCollection, Postman, Insomnia, OpenAPI v3, WSDL, and ZIP formats

    ); }; export default FileTab; ================================================ FILE: packages/bruno-app/src/components/Sidebar/ImportCollection/FullscreenLoader/index.js ================================================ import { useState, useEffect } from 'react'; import { IconLoader2 } from '@tabler/icons'; // Messages to cycle through while loading const loadingMessages = [ 'Processing collection...', 'Analyzing requests...', 'Translating scripts...', 'Preparing collection...', 'Almost done...' ]; const FullscreenLoader = ({ isLoading }) => { const [loadingMessage, setLoadingMessage] = useState(''); useEffect(() => { if (!isLoading) return; let messageIndex = 0; const interval = setInterval(() => { messageIndex = (messageIndex + 1) % loadingMessages.length; setLoadingMessage(loadingMessages[messageIndex]); }, 2000); setLoadingMessage(loadingMessages[0]); return () => clearInterval(interval); }, [isLoading]); return (

    {loadingMessage}

    This may take a moment depending on the collection size

    ); }; export default FullscreenLoader; ================================================ FILE: packages/bruno-app/src/components/Sidebar/ImportCollection/GitHubTab.js ================================================ import React, { useState } from 'react'; import { isGitRepositoryUrl } from 'utils/git'; import toast from 'react-hot-toast'; import Button from 'ui/Button'; const GitHubTab = ({ handleSubmit, setErrorMessage }) => { const [urlInput, setUrlInput] = useState(''); const handleGitRepositoryImport = (url) => { if (!isGitRepositoryUrl(url)) { setErrorMessage('Please enter a valid git repository URL'); return; } handleSubmit({ repositoryUrl: url, type: 'git-repository' }); }; const handleFormSubmit = (e) => { e.preventDefault(); if (urlInput.trim()) { handleGitRepositoryImport(urlInput.trim()); } }; return (
    setUrlInput(e.target.value)} placeholder="Enter Git repository URL" className="flex-1 px-3 py-1 textbox" />
    ); }; export default GitHubTab; ================================================ FILE: packages/bruno-app/src/components/Sidebar/ImportCollection/StyledWrapper.js ================================================ import styled from 'styled-components'; const StyledWrapper = styled.div` .tabs { .tab { padding: 6px 0px; border: none; border-bottom: solid 2px transparent; margin-right: 1.25rem; color: var(--color-tab-inactive); cursor: pointer; &:focus, &:active, &:focus-within, &:focus-visible, &:target { outline: none !important; box-shadow: none !important; } &.active { color: ${(props) => props.theme.tabs.active.color} !important; border-bottom: solid 2px ${(props) => props.theme.tabs.active.border} !important; } } } `; export default StyledWrapper; ================================================ FILE: packages/bruno-app/src/components/Sidebar/ImportCollection/UrlTab.js ================================================ import React, { useState } from 'react'; import { fetchAndValidateApiSpecFromUrl } from 'utils/importers/common'; import { isValidUrl } from 'utils/url/index'; import Button from 'ui/Button'; const UrlTab = ({ setIsLoading, handleSubmit, setErrorMessage }) => { const [urlInput, setUrlInput] = useState(''); const handleUrlImport = async (event) => { event.preventDefault(); if (!urlInput.trim() || !isValidUrl(urlInput.trim())) { setErrorMessage('Please enter a valid URL'); return; } setIsLoading(true); try { const { data, specType, rawContent } = await fetchAndValidateApiSpecFromUrl({ url: urlInput.trim() }); // Pass raw data for all types, include sourceUrl and rawContent for OpenAPI sync handleSubmit({ rawData: data, type: specType, sourceUrl: urlInput.trim(), rawContent }); } catch (err) { console.error(err); setErrorMessage('URL import failed. Please check the URL and try again.'); } finally { setIsLoading(false); } }; return (
    { setUrlInput(e.target.value); setErrorMessage(''); }} placeholder="Enter URL (OpenAPI/Swagger, Postman, or Insomnia specification)" className="flex-1 px-3 py-1 textbox" />
    ); }; export default UrlTab; ================================================ FILE: packages/bruno-app/src/components/Sidebar/ImportCollection/index.js ================================================ import React, { useState } from 'react'; import { IconFileImport, IconBrandGit, IconUnlink, IconX } from '@tabler/icons'; import Modal from 'components/Modal'; import classnames from 'classnames'; import StyledWrapper from './StyledWrapper'; import FileTab from './FileTab'; import GitHubTab from './GitHubTab'; import UrlTab from './UrlTab'; import FullscreenLoader from './FullscreenLoader/index'; import { useTheme } from 'providers/Theme'; const IMPORT_TABS = { FILE: 'file', GITHUB: 'github', URL: 'url' }; const ImportCollection = ({ onClose, handleSubmit }) => { const { theme } = useTheme(); const [isLoading, setIsLoading] = useState(false); const [errorMessage, setErrorMessage] = useState(''); const [tab, setTab] = useState(IMPORT_TABS.FILE); const handleTabSelect = (value) => () => { setTab(value); setErrorMessage(''); }; const getTabClassname = (tabName) => { return classnames(`flex tab items-center py-2 px-4 ${tabName}`, { active: tabName === tab }); }; if (isLoading) { return ; } return (
    File
    Git Repository
    URL
    {errorMessage && (
    {errorMessage}
    setErrorMessage('')} style={{ color: theme.status.danger.text }} >
    )} {tab === IMPORT_TABS.FILE && ( )} {tab === IMPORT_TABS.GITHUB && ( )} {tab === IMPORT_TABS.URL && ( )}
    ); }; export default ImportCollection; ================================================ FILE: packages/bruno-app/src/components/Sidebar/ImportCollectionLocation/StyledWrapper.js ================================================ import styled from 'styled-components'; import { darken, rgba } from 'polished'; const Wrapper = styled.div` .current-group { background-color: ${(props) => props.theme.background.surface1}; border-radius: 4px; padding: 0.3rem 0.6rem; cursor: pointer; border: 1px solid ${(props) => props.theme.background.surface2}; } .current-group:hover { background-color: ${(props) => darken(0.03, props.theme.background.surface1)}; border-color: ${(props) => darken(0.03, props.theme.background.surface2)}; /* Fix dropdown positioning */ [data-tippy-root] { left: 0 !important; } `; export default Wrapper; ================================================ FILE: packages/bruno-app/src/components/Sidebar/ImportCollectionLocation/index.js ================================================ import React, { useRef, useEffect, useState, forwardRef } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { useFormik } from 'formik'; import * as Yup from 'yup'; import get from 'lodash/get'; import path from 'utils/common/path'; import { IconCaretDown } from '@tabler/icons'; import { browseDirectory } from 'providers/ReduxStore/slices/collections/actions'; import { postmanToBruno } from 'utils/importers/postman-collection'; import { convertInsomniaToBruno } from 'utils/importers/insomnia-collection'; import { convertOpenapiToBruno } from 'utils/importers/openapi-collection'; import { processBrunoCollection } from 'utils/importers/bruno-collection'; import { processOpenCollection } from 'utils/importers/opencollection'; import { wsdlToBruno } from '@usebruno/converters'; import { toastError } from 'utils/common/error'; import { useBetaFeature, BETA_FEATURES } from 'utils/beta-features'; import Modal from 'components/Modal'; import Help from 'components/Help'; import Dropdown from 'components/Dropdown'; import StyledWrapper from './StyledWrapper'; import { DEFAULT_COLLECTION_FORMAT } from 'utils/common/constants'; // Extract collection name from raw data const getCollectionName = (format, rawData) => { if (!rawData) return 'Collection'; switch (format) { case 'openapi': return rawData.info?.title || 'OpenAPI Collection'; case 'postman': return rawData.info?.name || rawData.collection?.info?.name || 'Postman Collection'; case 'insomnia': // For Insomnia v4 format, name is in the workspace resource if (rawData.resources && Array.isArray(rawData.resources)) { const workspace = rawData.resources.find((r) => r._type === 'workspace'); if (workspace?.name) { return workspace.name; } } // Fallback to root name property return rawData.name || 'Insomnia Collection'; case 'bruno': return rawData.name || 'Bruno Collection'; case 'opencollection': return rawData.info?.name || 'OpenCollection'; case 'wsdl': return 'WSDL Collection'; case 'bruno-zip': return rawData.collectionName || 'Bruno Collection'; default: return 'Collection'; } }; // Convert raw data to Bruno collection format const convertCollection = async (format, rawData, groupingType, collectionFormat) => { try { let collection; switch (format) { case 'openapi': collection = convertOpenapiToBruno(rawData, { groupBy: groupingType, collectionFormat }); break; case 'wsdl': collection = await wsdlToBruno(rawData); break; case 'postman': collection = await postmanToBruno(rawData); break; case 'insomnia': collection = convertInsomniaToBruno(rawData); break; case 'bruno': collection = await processBrunoCollection(rawData); break; case 'opencollection': collection = await processOpenCollection(rawData); break; case 'bruno-zip': // ZIP doesn't need conversion collection = rawData; break; default: throw new Error('Unknown collection format'); } return collection; } catch (err) { console.error('Conversion error:', err); toastError(err, 'Failed to convert collection'); throw err; } }; const groupingOptions = [ { value: 'tags', label: 'Tags', description: 'Group requests by OpenAPI tags', testId: 'grouping-option-tags' }, { value: 'path', label: 'Paths', description: 'Group requests by URL path structure', testId: 'grouping-option-path' } ]; const ImportCollectionLocation = ({ onClose, handleSubmit, rawData, format, sourceUrl, filePath, rawContent }) => { const inputRef = useRef(); const dispatch = useDispatch(); const [groupingType, setGroupingType] = useState('tags'); const [collectionFormat, setCollectionFormat] = useState(DEFAULT_COLLECTION_FORMAT); const isOpenAPISyncEnabled = useBetaFeature(BETA_FEATURES.OPENAPI_SYNC); const [enableCheckForSpecUpdates, setEnableCheckForSpecUpdates] = useState(isOpenAPISyncEnabled); const dropdownTippyRef = useRef(); const isOpenApi = format === 'openapi'; const isZipImport = format === 'bruno-zip'; const isOpenApiFromUrl = isOpenApi && !!sourceUrl && !filePath; const isOpenApiFromFile = isOpenApi && !!filePath && !sourceUrl; const showCheckForSpecUpdatesOption = isOpenAPISyncEnabled && (isOpenApiFromUrl || isOpenApiFromFile); const { workspaces, activeWorkspaceUid } = useSelector((state) => state.workspaces); const preferences = useSelector((state) => state.app.preferences); const activeWorkspace = workspaces.find((w) => w.uid === activeWorkspaceUid); const isDefaultWorkspace = !activeWorkspace || activeWorkspace.type === 'default'; const defaultLocation = isDefaultWorkspace ? get(preferences, 'general.defaultLocation', '') : (activeWorkspace?.pathname ? path.join(activeWorkspace.pathname, 'collections') : ''); const collectionName = getCollectionName(format, rawData); const formik = useFormik({ enableReinitialize: true, initialValues: { collectionLocation: defaultLocation }, validationSchema: Yup.object({ collectionLocation: Yup.string() .min(1, 'must be at least 1 character') .max(500, 'must be 500 characters or less') .required('Location is required') }), onSubmit: async (values) => { const convertedCollection = await convertCollection(format, rawData, groupingType, collectionFormat); const options = { format: collectionFormat }; if (showCheckForSpecUpdatesOption && enableCheckForSpecUpdates) { const syncSourceUrl = sourceUrl || filePath; // URL or absolute path (backend converts to relative) const baseBrunoConfig = { version: convertedCollection.version || '1', name: convertedCollection.name || 'Untitled Collection', type: 'collection', ignore: ['node_modules', '.git'] }; convertedCollection.brunoConfig = { ...baseBrunoConfig, ...convertedCollection.brunoConfig, openapi: [ { sourceUrl: syncSourceUrl, groupBy: groupingType, autoCheck: true, autoCheckInterval: 5 } ] }; options.rawOpenAPISpec = rawContent || rawData; } handleSubmit(convertedCollection, values.collectionLocation, options); } }); const onDropdownCreate = (ref) => { dropdownTippyRef.current = ref; }; const GroupingDropdownIcon = forwardRef((props, ref) => { const selectedOption = groupingOptions.find((option) => option.value === groupingType); return (
    {selectedOption.label}
    ); }); const browse = () => { dispatch(browseDirectory()) .then((dirPath) => { if (typeof dirPath === 'string' && dirPath.length > 0) { formik.setFieldValue('collectionLocation', dirPath); } }) .catch((error) => { formik.setFieldValue('collectionLocation', ''); console.error(error); }); }; useEffect(() => { if (inputRef && inputRef.current) { inputRef.current.focus(); } }, [inputRef]); const onSubmit = async () => { if (isZipImport) { const errors = await formik.validateForm(); if (Object.keys(errors).length > 0) { formik.setTouched({ collectionLocation: true }); return; } const collectionLocation = formik.values.collectionLocation; handleSubmit(rawData, collectionLocation, { format: collectionFormat, isZipImport: true }); } else { formik.handleSubmit(); } }; return (
    e.preventDefault()}>
    {collectionName}
    <> { formik.setFieldValue('collectionLocation', e.target.value); }} /> {formik.touched.collectionLocation && formik.errors.collectionLocation ? (
    {formik.errors.collectionLocation}
    ) : null}
    Browse
    {!isZipImport && (
    )}
    {isOpenApi && (

    Select whether to create folders according to the spec's paths or tags.

    } placement="bottom-start"> {groupingOptions.map((option) => (
    { dropdownTippyRef?.current?.hide(); setGroupingType(option.value); }} > {option.label}
    ))}
    )} {showCheckForSpecUpdatesOption && (

    Stay notified of spec changes and sync your collection with the spec.

    )}
    ); }; export default ImportCollectionLocation; ================================================ FILE: packages/bruno-app/src/components/Sidebar/NewFolder/StyledWrapper.js ================================================ import styled from 'styled-components'; const StyledWrapper = styled.div` .advanced-options { .caret { color: ${(props) => props.theme.textLink}; fill: ${(props) => props.theme.textLink}; } } `; export default StyledWrapper; ================================================ FILE: packages/bruno-app/src/components/Sidebar/NewFolder/index.js ================================================ import React, { useRef, useEffect, useState, forwardRef } from 'react'; import { useFormik } from 'formik'; import toast from 'react-hot-toast'; import * as Yup from 'yup'; import Portal from 'components/Portal'; import Modal from 'components/Modal'; import { useDispatch } from 'react-redux'; import { newFolder } from 'providers/ReduxStore/slices/collections/actions'; import { IconArrowBackUp, IconEdit } from '@tabler/icons'; import { sanitizeName, validateName, validateNameError } from 'utils/common/regex'; import PathDisplay from 'components/PathDisplay/index'; import Help from 'components/Help'; import Dropdown from 'components/Dropdown'; import { IconCaretDown } from '@tabler/icons'; import StyledWrapper from './StyledWrapper'; import Button from 'ui/Button'; const NewFolder = ({ collectionUid, item, onClose }) => { const dispatch = useDispatch(); const inputRef = useRef(); const [isEditing, toggleEditing] = useState(false); const [showFilesystemName, toggleShowFilesystemName] = useState(false); const dropdownTippyRef = useRef(); const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref); const formik = useFormik({ enableReinitialize: true, initialValues: { folderName: '', directoryName: '' }, validationSchema: Yup.object({ folderName: Yup.string() .trim() .min(1, 'must be at least 1 character') .required('name is required'), directoryName: Yup.string() .trim() .min(1, 'must be at least 1 character') .required('foldername is required') .test('is-valid-folder-name', function (value) { const isValid = validateName(value); return isValid ? true : this.createError({ message: validateNameError(value) }); }) .test({ name: 'folderName', message: 'The folder name "environments" at the root of the collection is reserved in bruno', test: (value) => { if (item?.uid) return true; return value && !value.trim().toLowerCase().includes('environments'); } }) }), onSubmit: (values) => { dispatch(newFolder(values.folderName, values.directoryName, collectionUid, item ? item.uid : null)) .then(() => { toast.success('New folder created!'); onClose(); }) .catch((err) => toast.error(err ? err.message : 'An error occurred while adding the folder')); } }); useEffect(() => { if (inputRef && inputRef.current) { inputRef.current.focus(); } }, [inputRef]); const AdvancedOptions = forwardRef((props, ref) => { return (
    ); }); return (
    { formik.setFieldValue('folderName', e.target.value); !isEditing && formik.setFieldValue('directoryName', sanitizeName(e.target.value)); }} value={formik.values.folderName || ''} /> {formik.touched.folderName && formik.errors.folderName ? (
    {formik.errors.folderName}
    ) : null} {showFilesystemName && (
    {isEditing ? ( toggleEditing(false)} /> ) : ( toggleEditing(true)} /> )}
    {isEditing ? (
    ) : (
    )} {formik.touched.directoryName && formik.errors.directoryName ? (
    {formik.errors.directoryName}
    ) : null}
    )}
    } placement="bottom-start">
    { dropdownTippyRef.current.hide(); toggleShowFilesystemName(!showFilesystemName); }} > {showFilesystemName ? 'Hide Filesystem Name' : 'Show Filesystem Name'}
    ); }; export default NewFolder; ================================================ FILE: packages/bruno-app/src/components/Sidebar/NewRequest/StyledWrapper.js ================================================ import styled from 'styled-components'; const StyledWrapper = styled.div` div.method-selector-container { border: solid 1px ${(props) => props.theme.input.border}; border-right: none; background-color: ${(props) => props.theme.input.bg}; border-top-left-radius: ${(props) => props.theme.border.radius.base}; border-bottom-left-radius: ${(props) => props.theme.border.radius.base}; } div.method-selector-container, div.input-container { background-color: ${(props) => props.theme.input.bg}; height: 2.1rem; } div.input-container { border: solid 1px ${(props) => props.theme.input.border}; border-top-right-radius: ${(props) => props.theme.border.radius.base}; border-bottom-right-radius: ${(props) => props.theme.border.radius.base}; input { background-color: ${(props) => props.theme.input.bg}; outline: none; box-shadow: none; &:focus { outline: none !important; box-shadow: none !important; } } } .textbox { border-radius: ${(props) => props.theme.border.radius.base} !important; height: 2.1rem; } textarea.curl-command { min-height: 150px; } .dropdown { width: fit-content; .dropdown-item { padding: 0.2rem 0.6rem !important; } } .advanced-options { .caret { color: ${(props) => props.theme.textLink}; fill: ${(props) => props.theme.textLink}; } } `; export default StyledWrapper; ================================================ FILE: packages/bruno-app/src/components/Sidebar/NewRequest/index.js ================================================ import React, { useRef, useEffect, useCallback, forwardRef, useState } from 'react'; import get from 'lodash/get'; import { useFormik } from 'formik'; import * as Yup from 'yup'; import toast from 'react-hot-toast'; import path from 'utils/common/path'; import { uuid } from 'utils/common'; import Modal from 'components/Modal'; import { useDispatch, useSelector } from 'react-redux'; import { newEphemeralHttpRequest } from 'providers/ReduxStore/slices/collections'; import { newHttpRequest, newGrpcRequest, newWsRequest } from 'providers/ReduxStore/slices/collections/actions'; import { addTab } from 'providers/ReduxStore/slices/tabs'; import HttpMethodSelector from 'components/RequestPane/QueryUrl/HttpMethodSelector'; import { getDefaultRequestPaneTab } from 'utils/collections'; import { getRequestFromCurlCommand } from 'utils/curl'; import { IconArrowBackUp, IconCaretDown, IconEdit } from '@tabler/icons'; import { sanitizeName, validateName, validateNameError } from 'utils/common/regex'; import Dropdown from 'components/Dropdown'; import PathDisplay from 'components/PathDisplay'; import Portal from 'components/Portal'; import Help from 'components/Help'; import StyledWrapper from './StyledWrapper'; import SingleLineEditor from 'components/SingleLineEditor/index'; import { useTheme } from 'styled-components'; import Button from 'ui/Button'; const NewRequest = ({ collectionUid, item, isEphemeral, onClose }) => { const dispatch = useDispatch(); const inputRef = useRef(); const storedTheme = useTheme(); const collection = useSelector((state) => state.collections.collections?.find((c) => c.uid === collectionUid)); const collectionPresets = get( collection, collection?.draft?.brunoConfig ? 'draft.brunoConfig.presets' : 'brunoConfig.presets', {} ); const [curlRequestTypeDetected, setCurlRequestTypeDetected] = useState(null); const [showFilesystemName, toggleShowFilesystemName] = useState(false); const dropdownTippyRef = useRef(); const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref); const advancedDropdownTippyRef = useRef(); const onAdvancedDropdownCreate = (ref) => (advancedDropdownTippyRef.current = ref); const Icon = forwardRef((props, ref) => { return (
    {curlRequestTypeDetected === 'http-request' ? 'HTTP' : 'GraphQL'}
    ); }); // This function analyzes a given cURL command string and determines whether the request is a GraphQL or HTTP request. const identifyCurlRequestType = (url, headers, body) => { if (url.endsWith('/graphql')) { setCurlRequestTypeDetected('graphql-request'); return; } const contentType = headers?.find((h) => h.name.toLowerCase() === 'content-type')?.value; if (contentType && contentType.includes('application/graphql')) { setCurlRequestTypeDetected('graphql-request'); return; } setCurlRequestTypeDetected('http-request'); }; const curlRequestTypeChange = (type) => { setCurlRequestTypeDetected(type); }; const [isEditing, toggleEditing] = useState(false); const getRequestType = (collectionPresets) => { if (!collectionPresets || !collectionPresets.requestType) { return 'http-request'; } // Note: Why different labels for the same thing? // http-request and graphql-request are used inside the app's json representation of a request // http and graphql are used in Bru DSL as well as collection exports // We need to eventually standardize the app's DSL to use the same labels as bru DSL if (collectionPresets.requestType === 'http') { return 'http-request'; } if (collectionPresets.requestType === 'graphql') { return 'graphql-request'; } if (collectionPresets.requestType === 'grpc') { return 'grpc-request'; } if (collectionPresets.requestType === 'ws') { return 'ws-request'; } return 'http-request'; }; const formik = useFormik({ enableReinitialize: true, initialValues: { requestName: '', filename: '', requestType: getRequestType(collectionPresets), requestUrl: collectionPresets.requestUrl || '', requestMethod: 'GET', curlCommand: '' }, validationSchema: Yup.object({ requestName: Yup.string() .trim() .min(1, 'must be at least 1 character') .max(255, 'must be 255 characters or less') .required('name is required'), filename: Yup.string() .trim() .min(1, 'must be at least 1 character') .max(255, 'must be 255 characters or less') .required('filename is required') .test('is-valid-filename', function (value) { const isValid = validateName(value); return isValid ? true : this.createError({ message: validateNameError(value) }); }) .test( 'not-reserved', `The file names "collection" and "folder" are reserved in bruno`, (value) => !['collection', 'folder'].includes(value) ), curlCommand: Yup.string().when('requestType', { is: (requestType) => requestType === 'from-curl', then: Yup.string() .min(1, 'must be at least 1 character') .required('curlCommand is required') .test({ name: 'curlCommand', message: `Invalid cURL Command`, test: (value) => getRequestFromCurlCommand(value) !== null }) }) }), onSubmit: (values) => { const isGrpcRequest = values.requestType === 'grpc-request'; const isWsRequest = values.requestType === 'ws-request'; const filename = values.filename; if (isGrpcRequest) { dispatch( newGrpcRequest({ requestName: values.requestName, filename: filename, requestType: values.requestType, requestUrl: values.requestUrl, collectionUid: collection.uid, itemUid: item ? item.uid : null }) ) .then(() => { toast.success('New request created!'); onClose(); }) .catch((err) => toast.error(err ? err.message : 'An error occurred while adding the request')); // will need to handle import from grpcurl command when we support it, now it is just for creating new requests } else if (isWsRequest) { dispatch(newWsRequest({ requestName: values.requestName, requestMethod: values.requestMethod, filename: filename, requestType: values.requestType, requestUrl: values.requestUrl, collectionUid: collection.uid, itemUid: item ? item.uid : null })) .then(() => { toast.success('New request created!'); onClose(); }) .catch((err) => toast.error(err ? err.message : 'An error occurred while adding the request')); } else if (isEphemeral) { const uid = uuid(); dispatch( newEphemeralHttpRequest({ uid: uid, requestName: values.requestName, filename: filename, requestType: values.requestType, requestUrl: values.requestUrl, requestMethod: values.requestMethod, collectionUid: collectionUid }) ) .then(() => { dispatch( addTab({ uid: uid, collectionUid: collectionUid, requestPaneTab: getDefaultRequestPaneTab({ type: values.requestType }) }) ); onClose(); }) .catch((err) => toast.error(err ? err.message : 'An error occurred while adding the request')); } else if (values.requestType === 'from-curl') { const request = getRequestFromCurlCommand(values.curlCommand, curlRequestTypeDetected); const settings = { encodeUrl: false }; dispatch( newHttpRequest({ requestName: values.requestName, filename: filename, requestType: curlRequestTypeDetected, requestUrl: request.url, requestMethod: request.method, collectionUid: collectionUid, itemUid: item ? item.uid : null, headers: request.headers, body: request.body, auth: request.auth, settings: settings }) ) .then(() => { toast.success('New request created!'); onClose(); }) .catch((err) => toast.error(err ? err.message : 'An error occurred while adding the request')); } else { dispatch( newHttpRequest({ requestName: values.requestName, filename: filename, requestType: values.requestType, requestUrl: values.requestUrl, requestMethod: values.requestMethod, collectionUid: collectionUid, itemUid: item ? item.uid : null }) ) .then(() => { toast.success('New request created!'); onClose(); }) .catch((err) => toast.error(err ? err.message : 'An error occurred while adding the request')); } } }); useEffect(() => { if (inputRef && inputRef.current) { inputRef.current.focus(); } }, [inputRef]); const onSubmit = () => formik.handleSubmit(); const handlePaste = useCallback( (event) => { const clipboardData = event.clipboardData || window.clipboardData; const pastedData = clipboardData.getData('Text'); // Check if pasted data looks like a cURL command const curlCommandRegex = /^\s*curl\s/i; if (curlCommandRegex.test(pastedData)) { // Switch to the 'from-curl' request type formik.setFieldValue('requestType', 'from-curl'); formik.setFieldValue('curlCommand', pastedData); // Identify the request type const request = getRequestFromCurlCommand(pastedData); if (request) { identifyCurlRequestType(request.url, request.headers, request.body); } // Prevent the default paste behavior to avoid pasting into the textarea event.preventDefault(); } }, [formik] ); const handleCurlCommandChange = (event) => { formik.handleChange(event); if (event.target.name === 'curlCommand') { const curlCommand = event.target.value; const request = getRequestFromCurlCommand(curlCommand); if (request) { identifyCurlRequestType(request.url, request.headers, request.body); } } }; const AdvancedOptions = forwardRef((props, ref) => { return (
    ); }); return (
    { if (e.key === 'Enter') { e.preventDefault(); formik.handleSubmit(); } }} >
    { formik.setFieldValue('requestName', e.target.value); !isEditing && formik.setFieldValue('filename', sanitizeName(e.target.value)); }} value={formik.values.requestName || ''} data-testid="request-name" /> {formik.touched.requestName && formik.errors.requestName ? (
    {formik.errors.requestName}
    ) : null}
    {showFilesystemName && (
    {isEditing ? ( toggleEditing(false)} /> ) : ( toggleEditing(true)} /> )}
    {isEditing ? (
    .{collection.format}
    ) : (
    )} {formik.touched.filename && formik.errors.filename ? (
    {formik.errors.filename}
    ) : null}
    )} {formik.values.requestType !== 'from-curl' ? ( <>
    {!['grpc-request', 'ws-request'].includes(formik.values.requestType) ? (
    formik.setFieldValue('requestMethod', val)} showCaret />
    ) : null}
    { formik.handleChange({ target: { name: 'requestUrl', value: value } }); }} collection={collection} variablesAutocomplete={true} />
    {formik.touched.requestUrl && formik.errors.requestUrl ? (
    {formik.errors.requestUrl}
    ) : null}
    ) : (
    } placement="bottom-end">
    { dropdownTippyRef.current.hide(); curlRequestTypeChange('http-request'); }} > HTTP
    { dropdownTippyRef.current.hide(); curlRequestTypeChange('graphql-request'); }} > GraphQL
    {formik.touched.curlCommand && formik.errors.curlCommand ? (
    {formik.errors.curlCommand}
    ) : null}
    )}
    } placement="bottom-start">
    { advancedDropdownTippyRef.current.hide(); toggleShowFilesystemName(!showFilesystemName); }} > {showFilesystemName ? 'Hide Filesystem Name' : 'Show Filesystem Name'}
    ); }; export default NewRequest; ================================================ FILE: packages/bruno-app/src/components/Sidebar/Sections/ApiSpecsSection/index.js ================================================ import { useState } from 'react'; import toast from 'react-hot-toast'; import { useDispatch } from 'react-redux'; import { IconFileCode, IconPlus } from '@tabler/icons'; import { openApiSpec } from 'providers/ReduxStore/slices/apiSpec'; import MenuDropdown from 'ui/MenuDropdown'; import ActionIcon from 'ui/ActionIcon'; import CreateApiSpec from 'components/Sidebar/ApiSpecs/CreateApiSpec'; import ApiSpecs from 'components/Sidebar/ApiSpecs'; import SidebarSection from 'components/Sidebar/SidebarSection'; const ApiSpecsSection = () => { const dispatch = useDispatch(); const [createApiSpecModalOpen, setCreateApiSpecModalOpen] = useState(false); const handleOpenApiSpec = () => { dispatch(openApiSpec()).catch((err) => { console.error(err); toast.error('An error occurred while opening the API spec'); }); }; const addDropdownItems = [ { id: 'create-api-spec', leftSection: IconPlus, label: 'Create API Spec', onClick: () => { setCreateApiSpecModalOpen(true); } }, { id: 'open-api-spec', leftSection: IconFileCode, label: 'Open API Spec', onClick: () => { handleOpenApiSpec(); } } ]; const sectionActions = ( <> ); return ( <> {createApiSpecModalOpen && ( setCreateApiSpecModalOpen(false)} /> )} ); }; export default ApiSpecsSection; ================================================ FILE: packages/bruno-app/src/components/Sidebar/Sections/CollectionsSection/index.js ================================================ import { useState, useMemo } from 'react'; import toast from 'react-hot-toast'; import get from 'lodash/get'; import { useDispatch, useSelector } from 'react-redux'; import { IconArrowsSort, IconDotsVertical, IconDownload, IconFolder, IconPlus, IconSearch, IconSortAscendingLetters, IconSortDescendingLetters, IconSquareX, IconBox, IconTerminal2 } from '@tabler/icons'; import { importCollection, openCollection, importCollectionFromZip, newHttpRequest } from 'providers/ReduxStore/slices/collections/actions'; import { sortCollections } from 'providers/ReduxStore/slices/collections/index'; import { savePreferences, setIsCreatingCollection } from 'providers/ReduxStore/slices/app'; import { normalizePath } from 'utils/common/path'; import { isScratchCollection, flattenItems, isItemTransientRequest } from 'utils/collections'; import { sanitizeName } from 'utils/common/regex'; import filter from 'lodash/filter'; import MenuDropdown from 'ui/MenuDropdown'; import ActionIcon from 'ui/ActionIcon'; import ImportCollection from 'components/Sidebar/ImportCollection'; import ImportCollectionLocation from 'components/Sidebar/ImportCollectionLocation'; import BulkImportCollectionLocation from 'components/Sidebar/BulkImportCollectionLocation'; import CloneGitRepository from 'components/Sidebar/CloneGitRespository'; import RemoveCollectionsModal from 'components/Sidebar/Collections/RemoveCollectionsModal/index'; import CreateCollection from 'components/Sidebar/CreateCollection'; import WelcomeModal from 'components/WelcomeModal'; import Collections from 'components/Sidebar/Collections'; import SidebarSection from 'components/Sidebar/SidebarSection'; import { openDevtoolsAndSwitchToTerminal } from 'utils/terminal'; const CollectionsSection = () => { const [showSearch, setShowSearch] = useState(false); const dispatch = useDispatch(); const { workspaces, activeWorkspaceUid } = useSelector((state) => state.workspaces); const activeWorkspace = workspaces.find((w) => w.uid === activeWorkspaceUid); const { collections } = useSelector((state) => state.collections); const { collectionSortOrder } = useSelector((state) => state.collections); const { isCreatingCollection } = useSelector((state) => state.app); const preferences = useSelector((state) => state.app.preferences); const [collectionsToClose, setCollectionsToClose] = useState([]); const [importData, setImportData] = useState(null); const [createCollectionModalOpen, setCreateCollectionModalOpen] = useState(false); const [advancedCreateName, setAdvancedCreateName] = useState(''); const [importCollectionModalOpen, setImportCollectionModalOpen] = useState(false); const [importCollectionLocationModalOpen, setImportCollectionLocationModalOpen] = useState(false); const [showCloneGitModal, setShowCloneGitModal] = useState(false); const [gitRepositoryUrl, setGitRepositoryUrl] = useState(null); // Default to true (don't show modal) so that: // 1. Existing users who upgrade (no hasSeenWelcomeModal in their prefs) don't see it // 2. The modal doesn't flash before preferences are loaded from the electron process // Only genuinely new users will have hasSeenWelcomeModal explicitly set to false by onboarding const hasSeenWelcomeModal = get(preferences, 'onboarding.hasSeenWelcomeModal', true); const showWelcomeModal = !hasSeenWelcomeModal; const handleDismissWelcomeModal = () => { const updatedPreferences = { ...preferences, onboarding: { ...preferences.onboarding, hasSeenWelcomeModal: true } }; dispatch(savePreferences(updatedPreferences)).catch(() => { toast.error('Failed to save preferences'); }); }; const workspaceCollections = useMemo(() => { if (!activeWorkspace) return []; return collections.filter((c) => { if (isScratchCollection(c, workspaces)) { return false; } return activeWorkspace.collections?.some((wc) => normalizePath(wc.path) === normalizePath(c.pathname)); }); }, [activeWorkspace, collections, workspaces]); const handleImportCollection = ({ rawData, type, repositoryUrl, ...rest }) => { setImportCollectionModalOpen(false); if (type === 'git-repository') { setGitRepositoryUrl(repositoryUrl); setShowCloneGitModal(true); return; } setImportData({ rawData, type, ...rest }); setImportCollectionLocationModalOpen(true); }; const handleImportCollectionLocation = (convertedCollection, collectionLocation, options = {}) => { const importAction = options.isZipImport ? importCollectionFromZip(convertedCollection.zipFilePath, collectionLocation) : importCollection(convertedCollection, collectionLocation, options); dispatch(importAction) .then(() => { setImportCollectionLocationModalOpen(false); setImportData(null); }); }; const handleCloseGitModal = () => { setShowCloneGitModal(false); setGitRepositoryUrl(null); }; const handleToggleSearch = () => { setShowSearch((prev) => !prev); }; const handleSortCollections = () => { let order; switch (collectionSortOrder) { case 'default': order = 'alphabetical'; break; case 'alphabetical': order = 'reverseAlphabetical'; break; case 'reverseAlphabetical': order = 'default'; break; default: order = 'default'; break; } dispatch(sortCollections({ order })); }; const getSortIcon = () => { switch (collectionSortOrder) { case 'alphabetical': return IconSortDescendingLetters; case 'reverseAlphabetical': return IconArrowsSort; default: return IconSortAscendingLetters; } }; const getSortLabel = () => { switch (collectionSortOrder) { case 'alphabetical': return 'Sort Z-A'; case 'reverseAlphabetical': return 'Clear sort'; default: return 'Sort A-Z'; } }; const selectAllCollectionsToClose = () => { setCollectionsToClose(workspaceCollections.map((c) => c.uid)); }; const clearCollectionsToClose = () => { setCollectionsToClose([]); }; const handleOpenCollection = () => { const options = {}; if (activeWorkspace?.pathname) { options.workspaceId = activeWorkspace.pathname; } dispatch(openCollection(options)).catch((err) => { toast.error('An error occurred while opening the collection'); }); }; const handleStartRequest = () => { const scratchCollectionUid = activeWorkspace?.scratchCollectionUid; if (!scratchCollectionUid) { toast.error('Unable to create request'); return; } const scratchCollection = collections.find((c) => c.uid === scratchCollectionUid); if (!scratchCollection) { toast.error('Unable to create request'); return; } const allItems = flattenItems(scratchCollection.items || []); const transientRequests = filter(allItems, (item) => isItemTransientRequest(item)); let maxNumber = 0; transientRequests.forEach((item) => { const match = item.name?.match(/^Untitled (\d+)$/); if (match) { const number = parseInt(match[1], 10); if (number > maxNumber) { maxNumber = number; } } }); const requestName = `Untitled ${maxNumber + 1}`; const filename = sanitizeName(requestName); dispatch( newHttpRequest({ requestName, filename, requestType: 'http-request', requestUrl: '', requestMethod: 'GET', collectionUid: scratchCollectionUid, itemUid: null, isTransient: true }) ).catch((err) => { toast.error('An error occurred while creating the request'); }); }; const handleOpenAdvancedCreate = (name) => { dispatch(setIsCreatingCollection(false)); setAdvancedCreateName(name || ''); setCreateCollectionModalOpen(true); }; const addDropdownItems = [ { id: 'create', leftSection: IconPlus, label: 'Create collection', onClick: () => { dispatch(setIsCreatingCollection(true)); } }, { id: 'open', leftSection: IconFolder, label: 'Open collection', onClick: () => { handleOpenCollection(); } }, { id: 'import', leftSection: IconDownload, label: 'Import collection', onClick: () => { setImportCollectionModalOpen(true); } } ]; const actionsDropdownItems = [ { id: 'sort', leftSection: getSortIcon(), label: getSortLabel(), onClick: () => { handleSortCollections(); } }, { id: 'close-all', leftSection: IconSquareX, label: 'Close all', onClick: () => { selectAllCollectionsToClose(); } }, { id: 'open-in-terminal', leftSection: IconTerminal2, label: 'Open in Terminal', onClick: () => { openDevtoolsAndSwitchToTerminal(dispatch, activeWorkspace?.pathname); } } ]; const sectionActions = ( <> {collectionsToClose.length > 0 && ( )} ); return ( <> {showWelcomeModal && ( { handleDismissWelcomeModal(); setImportCollectionModalOpen(true); }} onCreateCollection={() => { handleDismissWelcomeModal(); setCreateCollectionModalOpen(true); }} onOpenCollection={() => { handleDismissWelcomeModal(); handleOpenCollection(); }} onStartRequest={() => { handleDismissWelcomeModal(); handleStartRequest(); }} /> )} {createCollectionModalOpen && ( { setCreateCollectionModalOpen(false); setAdvancedCreateName(''); }} initialCollectionName={advancedCreateName} /> )} {importCollectionModalOpen && ( setImportCollectionModalOpen(false)} handleSubmit={handleImportCollection} /> )} {importCollectionLocationModalOpen && importData && (importData.type !== 'multiple' && importData.type !== 'bulk') && ( setImportCollectionLocationModalOpen(false)} handleSubmit={handleImportCollectionLocation} /> )} {importCollectionLocationModalOpen && importData && (importData.type === 'multiple' || importData.type === 'bulk') && ( setImportCollectionLocationModalOpen(false)} handleSubmit={handleImportCollectionLocation} /> )} {showCloneGitModal && ( )} dispatch(setIsCreatingCollection(true))} onDismissCreate={() => dispatch(setIsCreatingCollection(false))} onOpenAdvancedCreate={handleOpenAdvancedCreate} /> ); }; export default CollectionsSection; ================================================ FILE: packages/bruno-app/src/components/Sidebar/SidebarAccordionContext.js ================================================ import React, { createContext, useContext, useState, useCallback, useRef } from 'react'; const SidebarAccordionContext = createContext(); export const useSidebarAccordion = () => { const context = useContext(SidebarAccordionContext); if (!context) { throw new Error('useSidebarAccordion must be used within SidebarAccordionProvider'); } return context; }; export const SidebarAccordionProvider = ({ children, defaultExpanded = ['collections'] }) => { const [expandedSections, setExpandedSections] = useState(new Set(defaultExpanded)); const dropdownContainerRef = useRef(null); const toggleSection = useCallback((sectionId) => { setExpandedSections((prev) => { const newSet = new Set(prev); if (newSet.has(sectionId)) { newSet.delete(sectionId); } else { newSet.add(sectionId); } return newSet; }); }, []); const setSectionExpanded = useCallback((sectionId, expanded) => { setExpandedSections((prev) => { const newSet = new Set(prev); if (expanded) { newSet.add(sectionId); } else { newSet.delete(sectionId); } return newSet; }); }, []); const isExpanded = useCallback((sectionId) => { return expandedSections.has(sectionId); }, [expandedSections]); const getExpandedCount = useCallback(() => { return expandedSections.size; }, [expandedSections]); return (
    {children}
    ); }; ================================================ FILE: packages/bruno-app/src/components/Sidebar/SidebarContent.js ================================================ import { useSidebarAccordion } from './SidebarAccordionContext'; /** * Sections configuration * * All sections use the same generic accordion behavior with the class 'accordion-section-wrapper'. * Layout behavior is fully automatic based on section order and expansion state: * - Single expanded: When only one section is expanded, it fills available space * - Multi-expanded: When multiple sections are expanded, they split space equally * - Automatic pinning: Sections below an expanded section are automatically pinned to bottom * * To add a new section, simply add a new entry to this array: * * { * id: 'my-section', // Unique identifier * component: MySectionComponent, // React component to render * getProps: (context) => ({ ... }) // Function to get props for component * } */ const SidebarContent = ({ sections }) => { const { isExpanded, getExpandedCount } = useSidebarAccordion(); const expandedCount = getExpandedCount(); const getWrapperClassName = (section, sectionIndex) => { const sectionExpanded = isExpanded(section.id); // Use generic accordion-section-wrapper class for all sections const classes = ['accordion-section-wrapper']; // Multi-expanded: when multiple sections are expanded if (expandedCount > 1 && sectionExpanded) { classes.push('multi-expanded'); } // Single expanded wrapper behavior: when only one section is expanded, it fills space if (sectionExpanded && expandedCount === 1) { classes.push('single-expanded-wrapper'); } // Automatic pinning: if section is not expanded and any section above it (earlier in array) is expanded if (!sectionExpanded) { // Check if any section before this one (earlier in array) is expanded const hasExpandedAbove = sections.slice(0, sectionIndex).some((s) => isExpanded(s.id)); if (hasExpandedAbove) { classes.push('pinned-to-bottom'); } } return classes.join(' '); }; return ( <> {sections.map((section, index) => { const SectionComponent = section.component; const wrapperClassName = getWrapperClassName(section, index); return (
    ); })} ); }; export default SidebarContent; ================================================ FILE: packages/bruno-app/src/components/Sidebar/SidebarSection/StyledWrapper.js ================================================ import styled from 'styled-components'; const StyledWrapper = styled.div` height: 100%; .sidebar-section { display: flex; flex-direction: column; min-height: 0; height: 100%; &.expanded { flex: 1 1 0%; min-height: 0; } &:not(.expanded) { flex: 0 0 auto; } &.multi-expanded { flex: 1 1 0%; margin-bottom: 0; } } .section-header { display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 6px 4px 6px 8px; min-height: 28px; height: 28px; user-select: none; transition: background-color 0.15s ease; flex-shrink: 0; border-bottom: 1px solid transparent; .section-header-left { display: flex; align-items: center; gap: 6px; flex: 1; min-width: 0; cursor: pointer; &:hover { .section-toggle { display: flex; } .section-toggle { background: ${(props) => props.theme.dropdown.hoverBg}; color: ${(props) => props.theme.text} !important; } .section-icon { display: none; } } } } .section-icon-wrapper { width: 24px; height: 24px; flex-shrink: 0; display: flex; align-items: center; justify-content: center; } .section-toggle { display: none; } .section-icon { display: flex; align-items: center; justify-content: center; width: 16px; height: 16px; color: ${(props) => props.theme.sidebar.muted}; } .section-title { color: ${(props) => props.theme.sidebar.color}; font-size: 12px; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .section-actions { display: flex; align-items: center; gap: 1px; flex-shrink: 0; } } .section-content { display: flex; flex-direction: column; flex: 1 1 0%; min-height: 0; overflow-y: auto; overflow-x: hidden; position: relative; } `; export default StyledWrapper; ================================================ FILE: packages/bruno-app/src/components/Sidebar/SidebarSection/index.js ================================================ import { useState, useEffect, useRef } from 'react'; import { IconChevronRight, IconChevronDown } from '@tabler/icons'; import StyledWrapper from './StyledWrapper'; import { useSidebarAccordion } from '../SidebarAccordionContext'; import ActionIcon from 'ui/ActionIcon/index'; const SidebarSection = ({ id, title, icon: Icon, actions, children, className = '' }) => { const { isExpanded, setSectionExpanded, getExpandedCount } = useSidebarAccordion(); const [localExpanded, setLocalExpanded] = useState(() => isExpanded(id)); const sectionRef = useRef(null); // Sync with context useEffect(() => { const expanded = isExpanded(id); setLocalExpanded(expanded); }, [id, isExpanded]); const handleToggle = () => { const newExpanded = !localExpanded; setLocalExpanded(newExpanded); setSectionExpanded(id, newExpanded); }; const expandedCount = getExpandedCount(); // Check if this is the only expanded section const isOnlyExpanded = expandedCount === 1 && localExpanded; return (
    1 && localExpanded ? 'multi-expanded' : ''}`} >
    { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); handleToggle(); } }} > {localExpanded ? ( ) : ( )} {Icon && }
    {title}
    {actions && (
    { e.stopPropagation(); if (!localExpanded) { setSectionExpanded(id, true); } }} > {actions}
    )}
    {localExpanded && (
    {children}
    )}
    ); }; export default SidebarSection; ================================================ FILE: packages/bruno-app/src/components/Sidebar/StyledWrapper.js ================================================ import styled from 'styled-components'; const Wrapper = styled.div` color: ${(props) => props.theme.sidebar.color}; max-height: 100%; aside { background-color: ${(props) => props.theme.sidebar.bg}; overflow: hidden; .sidebar-sections-container { display: flex; flex-direction: column; } .sidebar-sections { min-height: 0; display: flex; flex-direction: column; height: 100%; } /* Expanded sections grow to fill available space but are constrained */ .sidebar-section.expanded { flex: 1 1 0%; min-height: 0; .section-header { border-bottom: 1px solid ${(props) => props.theme.sidebar.collection.item.hoverBg}; } } /* Single expanded section: add margin-bottom to push others down */ .sidebar-section.single-expanded { margin-bottom: auto !important; flex: 1 1 0% !important; min-height: 0; max-height: 100%; } /* Multiple expanded sections: equal split, no margin-bottom */ .sidebar-section.multi-expanded { margin-bottom: 0; flex: 1 1 0% !important; min-height: 0; overflow: hidden; max-height: 100%; } /* Collapsed sections only take header height */ .sidebar-section:not(.expanded) { flex: 0 0 auto; } /* Always push bottom accordions wrapper to the bottom */ .bottom-accordions-wrapper { display: flex; flex-direction: column; flex: 0 0 auto; } /* Generic accordion section wrapper - applies to all accordion sections */ .accordion-section-wrapper { display: flex; flex-direction: column; min-height: 0; position: relative; overflow: visible; } /* Add border-top to all accordion items except the first child */ .accordion-section-wrapper:not(:first-child) { border-top: 1px solid ${(props) => props.theme.sidebar.collection.item.hoverBg}; } /* When a section is single expanded, wrapper should fill space but respect pinned sections */ .accordion-section-wrapper.single-expanded-wrapper { flex: 1 1 0% !important; min-height: 0; overflow: hidden; } /* Normal flow: sections not pinned and not multi-expanded */ .accordion-section-wrapper:not(.pinned-to-bottom):not(.multi-expanded) { flex: 0 0 auto; } /* When a section is pinned to bottom */ .accordion-section-wrapper.pinned-to-bottom { flex: 0 0 auto; margin-top: auto; } /* When multiple sections are expanded, split space equally */ .accordion-section-wrapper.multi-expanded { flex: 1 1 0% !important; min-height: 0; margin-top: 0 !important; height: auto !important; } } div.sidebar-drag-handle { display: flex; align-items: center; justify-content: center; height: 100%; cursor: col-resize; background-color: transparent; width: 6px; right: -3px; transition: opacity 0.2s ease; div.drag-request-border { width: 1px; height: 100%; border-left: solid 1px ${(props) => props.theme.sidebar.dragbar.border}; } &:hover div.drag-request-border { width: 1px; height: 100%; border-left: solid 1px ${(props) => props.theme.sidebar.dragbar.activeBorder}; } } `; export default Wrapper; ================================================ FILE: packages/bruno-app/src/components/Sidebar/index.js ================================================ import { SidebarAccordionProvider } from './SidebarAccordionContext'; import SidebarContent from './SidebarContent'; import StyledWrapper from './StyledWrapper'; import { useState, useEffect, useRef } from 'react'; import { useSelector, useDispatch } from 'react-redux'; import { updateLeftSidebarWidth, updateIsDragging } from 'providers/ReduxStore/slices/app'; import CollectionsSection from './Sections/CollectionsSection/index'; import ApiSpecsSection from './Sections/ApiSpecsSection/index'; const MIN_LEFT_SIDEBAR_WIDTH = 220; const MAX_LEFT_SIDEBAR_WIDTH = 600; const SIDEBAR_SECTIONS = [ { id: 'collections', component: CollectionsSection }, { id: 'api-specs', component: ApiSpecsSection } ]; const Sidebar = () => { const leftSidebarWidth = useSelector((state) => state.app.leftSidebarWidth); const sidebarCollapsed = useSelector((state) => state.app.sidebarCollapsed); const [asideWidth, setAsideWidth] = useState(leftSidebarWidth); const lastWidthRef = useRef(leftSidebarWidth); const dispatch = useDispatch(); const [dragging, setDragging] = useState(false); const currentWidth = sidebarCollapsed ? 0 : asideWidth; // Clamp helper keeps width in allowed range const clamp = (value, min, max) => Math.min(max, Math.max(min, value)); const handleMouseMove = (e) => { if (!dragging || sidebarCollapsed) return; e.preventDefault(); const nextWidth = clamp(e.clientX + 2, MIN_LEFT_SIDEBAR_WIDTH, MAX_LEFT_SIDEBAR_WIDTH); if (Math.abs(nextWidth - lastWidthRef.current) < 3) return; lastWidthRef.current = nextWidth; setAsideWidth(nextWidth); }; const handleMouseUp = (e) => { if (dragging) { e.preventDefault(); setDragging(false); dispatch( updateLeftSidebarWidth({ leftSidebarWidth: asideWidth }) ); dispatch( updateIsDragging({ isDragging: false }) ); } }; const handleDragbarMouseDown = (e) => { e.preventDefault(); if (sidebarCollapsed) { return; } setDragging(true); dispatch( updateIsDragging({ isDragging: true }) ); }; useEffect(() => { document.addEventListener('mouseup', handleMouseUp); document.addEventListener('mousemove', handleMouseMove); return () => { document.removeEventListener('mouseup', handleMouseUp); document.removeEventListener('mousemove', handleMouseMove); }; }, [dragging, asideWidth]); useEffect(() => { setAsideWidth(leftSidebarWidth); }, [leftSidebarWidth]); return ( {!sidebarCollapsed && (
    )} ); }; export default Sidebar; ================================================ FILE: packages/bruno-app/src/components/SingleLineEditor/StyledWrapper.js ================================================ import styled from 'styled-components'; const StyledWrapper = styled.div` width: 100%; height: ${(props) => (props.$isCompact ? '1.375rem' : '1.875rem')}; overflow-y: hidden; overflow-x: hidden; &.read-only { .CodeMirror-cursor { display: none !important; } } .CodeMirror { background: transparent; height: ${(props) => (props.$isCompact ? '1.375rem' : '2.125rem')}; font-size: ${(props) => props.theme.font.size.base}; line-height: ${(props) => (props.$isCompact ? '1.375rem' : '1.875rem')}; overflow: hidden; .CodeMirror-scroll { overflow: hidden !important; padding-bottom: 3.125rem !important; } .CodeMirror-vscrollbar, .CodeMirror-hscrollbar, .CodeMirror-scrollbar-filler { display: none; } .CodeMirror-lines { padding: 0; .CodeMirror-placeholder { color: ${(props) => props.theme.codemirror.placeholder.color} !important; opacity: ${(props) => props.theme.codemirror.placeholder.opacity} !important } } .CodeMirror-cursor { height: ${(props) => (props.$isCompact ? '0.875rem' : '1.25rem')} !important; margin-top: ${(props) => (props.$isCompact ? '0.25rem' : '0.3125rem')} !important; border-left: 1px solid ${(props) => props.theme.text} !important; } pre { font-family: Inter, sans-serif !important; font-weight: 400; } .CodeMirror-line { color: ${(props) => props.theme.text}; padding: 0; } .CodeMirror-selected { background-color: rgba(212, 125, 59, 0.3); } } `; export default StyledWrapper; ================================================ FILE: packages/bruno-app/src/components/SingleLineEditor/index.js ================================================ import React, { Component } from 'react'; import isEqual from 'lodash/isEqual'; import { getAllVariables } from 'utils/collections'; import { defineCodeMirrorBrunoVariablesMode } from 'utils/common/codemirror'; import { MaskedEditor } from 'utils/common/masked-editor'; import { setupAutoComplete } from 'utils/codemirror/autocomplete'; import StyledWrapper from './StyledWrapper'; import { IconEye, IconEyeOff } from '@tabler/icons'; import { setupLinkAware } from 'utils/codemirror/linkAware'; const CodeMirror = require('codemirror'); class SingleLineEditor extends Component { constructor(props) { super(props); // Keep a cached version of the value, this cache will be updated when the // editor is updated, which can later be used to protect the editor from // unnecessary updates during the update lifecycle. this.cachedValue = props.value || ''; this.editorRef = React.createRef(); this.variables = {}; this.readOnly = props.readOnly || false; this.state = { maskInput: props.isSecret || false // Always mask the input by default (if it's a secret) }; } componentDidMount() { // Initialize CodeMirror as a single line editor /** @type {import("codemirror").Editor} */ const variables = getAllVariables(this.props.collection, this.props.item); const runHandler = () => { if (this.props.onRun) { this.props.onRun(); } }; const saveHandler = () => { if (this.props.onSave) { this.props.onSave(); } }; const noopHandler = () => { }; this.editor = CodeMirror(this.editorRef.current, { placeholder: this.props.placeholder ?? '', lineWrapping: false, lineNumbers: false, theme: this.props.theme === 'dark' ? 'monokai' : 'default', mode: 'brunovariables', brunoVarInfo: this.props.enableBrunoVarInfo !== false ? { variables, collection: this.props.collection, item: this.props.item } : false, scrollbarStyle: null, tabindex: 0, readOnly: this.props.readOnly, extraKeys: { 'Enter': runHandler, 'Ctrl-Enter': runHandler, 'Cmd-Enter': runHandler, 'Alt-Enter': () => { if (this.props.allowNewlines) { this.editor.setValue(this.editor.getValue() + '\n'); this.editor.setCursor({ line: this.editor.lineCount(), ch: 0 }); } else if (this.props.onRun) { this.props.onRun(); } }, 'Shift-Enter': runHandler, 'Cmd-S': saveHandler, 'Ctrl-S': saveHandler, 'Cmd-F': noopHandler, 'Ctrl-F': noopHandler, // Tabbing disabled to make tabindex work 'Tab': false, 'Shift-Tab': false } }); const getAllVariablesHandler = () => getAllVariables(this.props.collection, this.props.item); const getAnywordAutocompleteHints = () => this.props.autocomplete || []; // Setup AutoComplete Helper const autoCompleteOptions = { getAllVariables: getAllVariablesHandler, getAnywordAutocompleteHints, showHintsFor: this.props.showHintsFor || ['variables'], showHintsOnClick: this.props.showHintsOnClick }; this.brunoAutoCompleteCleanup = setupAutoComplete( this.editor, autoCompleteOptions ); this.editor.setValue(String(this.props.value ?? '')); this.editor.on('change', this._onEdit); this.editor.on('paste', this._onPaste); this.addOverlay(variables); this._enableMaskedEditor(this.props.isSecret); this.setState({ maskInput: this.props.isSecret }); // Add newline arrow markers if enabled if (this.props.showNewlineArrow) { this._updateNewlineMarkers(); } setupLinkAware(this.editor); } /** Enable or disable masking the rendered content of the editor */ _enableMaskedEditor = (enabled) => { if (typeof enabled !== 'boolean') return; if (enabled == true) { if (!this.maskedEditor) this.maskedEditor = new MaskedEditor(this.editor, '*'); this.maskedEditor.enable(); } else { if (this.maskedEditor) { this.maskedEditor.disable(); this.maskedEditor.destroy(); this.maskedEditor = null; } } }; _onEdit = () => { if (!this.ignoreChangeEvent && this.editor) { this.cachedValue = this.editor.getValue(); if (this.props.onChange && (this.props.value !== this.cachedValue)) { this.props.onChange(this.cachedValue); } // Update newline markers after edit if (this.props.showNewlineArrow) { this._updateNewlineMarkers(); } } }; _onPaste = (_, event) => this.props.onPaste?.(event); componentDidUpdate(prevProps) { // Ensure the changes caused by this update are not interpreted as // user-input changes which could otherwise result in an infinite // event loop. this.ignoreChangeEvent = true; let variables = getAllVariables(this.props.collection, this.props.item); if (!isEqual(variables, this.variables)) { if (this.props.enableBrunoVarInfo !== false && this.editor.options.brunoVarInfo) { this.editor.options.brunoVarInfo.variables = variables; } this.addOverlay(variables); } // Update collection and item when they change if (this.props.enableBrunoVarInfo !== false && this.editor.options.brunoVarInfo) { if (!isEqual(this.props.collection, this.editor.options.brunoVarInfo.collection)) { this.editor.options.brunoVarInfo.collection = this.props.collection; } if (!isEqual(this.props.item, this.editor.options.brunoVarInfo.item)) { this.editor.options.brunoVarInfo.item = this.props.item; } } if (this.props.theme !== prevProps.theme && this.editor) { this.editor.setOption('theme', this.props.theme === 'dark' ? 'monokai' : 'default'); } if (this.props.value !== prevProps.value && this.props.value !== this.cachedValue && this.editor) { // TODO: temporary fix for keeping cursor state when auto save and new line insertion collide PR#7098 const nextValue = String(this.props.value ?? ''); const currentValue = this.editor.getValue(); if (this.editor.hasFocus?.() && currentValue !== nextValue && nextValue !== '') { this.cachedValue = currentValue; } else { const cursor = this.editor.getCursor(); this.cachedValue = nextValue; this.editor.setValue(nextValue); this.editor.setCursor(cursor); // Update newline markers after value change if (this.props.showNewlineArrow) { this._updateNewlineMarkers(); } } } if (!isEqual(this.props.isSecret, prevProps.isSecret)) { // If the secret flag has changed, update the editor to reflect the change this._enableMaskedEditor(this.props.isSecret); // also set the maskInput flag to the new value this.setState({ maskInput: this.props.isSecret }); } if (this.props.readOnly !== prevProps.readOnly && this.editor) { this.editor.setOption('readOnly', this.props.readOnly); } if (this.props.placeholder !== prevProps.placeholder && this.editor) { this.editor.setOption('placeholder', this.props.placeholder); } this.ignoreChangeEvent = false; } componentWillUnmount() { if (this.editor) { if (this.editor?._destroyLinkAware) { this.editor._destroyLinkAware(); } this.editor.off('change', this._onEdit); this.editor.off('paste', this._onPaste); this._clearNewlineMarkers(); this.editor.getWrapperElement().remove(); this.editor = null; } if (this.brunoAutoCompleteCleanup) { this.brunoAutoCompleteCleanup(); } if (this.maskedEditor) { this.maskedEditor.destroy(); this.maskedEditor = null; } } addOverlay = (variables) => { this.variables = variables; defineCodeMirrorBrunoVariablesMode(variables, 'text/plain', this.props.highlightPathParams, true); this.editor.setOption('mode', 'brunovariables'); }; /** * Update markers to show arrows for newlines */ _updateNewlineMarkers = () => { if (!this.editor) return; // Clear existing markers this._clearNewlineMarkers(); this.newlineMarkers = []; const content = this.editor.getValue(); // Find all newlines and replace them with arrow widgets for (let i = 0; i < content.length; i++) { if (content[i] === '\n') { const pos = this.editor.posFromIndex(i); const nextPos = this.editor.posFromIndex(i + 1); // Create a widget to display the arrow const arrow = document.createElement('span'); arrow.className = 'newline-arrow'; arrow.textContent = '↲'; arrow.style.cssText = ` color: #888; font-size: 8px; margin: 0 2px; vertical-align: middle; display: inline-block; `; // Mark the newline character and replace it with the arrow widget const marker = this.editor.markText(pos, nextPos, { replacedWith: arrow, handleMouseEvents: true }); this.newlineMarkers.push(marker); } } }; /** * Clear all newline markers */ _clearNewlineMarkers = () => { if (this.newlineMarkers) { this.newlineMarkers.forEach((marker) => { try { marker.clear(); } catch (e) { // Marker might already be cleared } }); this.newlineMarkers = []; } }; toggleVisibleSecret = () => { const isVisible = !this.state.maskInput; this.setState({ maskInput: isVisible }); this._enableMaskedEditor(isVisible); }; /** * @brief Eye icon to show/hide the secret value * @returns ReactComponent The eye icon */ secretEye = (isSecret) => { return isSecret === true ? ( ) : null; }; render() { return (
    {this.secretEye(this.props.isSecret)}
    ); } } export default SingleLineEditor; ================================================ FILE: packages/bruno-app/src/components/Spinner/StyledWrapper.js ================================================ import styled from 'styled-components'; const StyledWrapper = styled.div` .spinner { display: inline-block; width: 2rem; height: 2rem; vertical-align: text-bottom; border: 0.25em solid currentColor; border-right-color: transparent; border-radius: 50%; animation: spinner-border 0.75s linear infinite; } `; export default StyledWrapper; ================================================ FILE: packages/bruno-app/src/components/Spinner/index.js ================================================ import React from 'react'; import StyledWrapper from './StyledWrapper'; // Todo: Size, Color config support const Spinner = ({ size, color, children }) => { return (
    {children &&
    {children}
    }
    ); }; export default Spinner; ================================================ FILE: packages/bruno-app/src/components/StatusBar/StyledWrapper.js ================================================ import styled from 'styled-components'; const StyledWrapper = styled.div` .status-bar { display: flex; align-items: center; justify-content: space-between; padding: 0 1rem; height: 1.5rem; background: ${(props) => props.theme.sidebar.bg}; border-top: 1px solid ${(props) => props.theme.statusBar.border}; color: ${(props) => props.theme.statusBar.color}; font-size: ${(props) => props.theme.font.size.sm}; user-select: none; position: relative; } .status-bar-section { display: flex; align-items: center; position: relative; } .status-bar-group { display: flex; align-items: center; gap: 2px; } .status-bar-button { display: flex; align-items: center; justify-content: center; padding: 0 4px; cursor: pointer; position: relative; outline: none; } .console-button-content { display: flex; align-items: center; justify-content: center; gap: 0.25rem; position: relative; } .console-label { white-space: nowrap; } .error-count-inline { font-size: 10px; font-weight: 500; color: ${(props) => props.theme.colors.text.danger}; background: ${(props) => props.theme.colors.bg.danger}20; padding: 1px 4px; border-radius: 4px; } .status-bar-divider { width: 1px; height: 16px; background: ${(props) => props.theme.sidebar.dragbar}; opacity: 0.4; } .status-bar-version { display: flex; align-items: center; padding: 2px 6px; } `; export default StyledWrapper; ================================================ FILE: packages/bruno-app/src/components/StatusBar/ThemeDropdown/StyledWrapper.js ================================================ import styled from 'styled-components'; import { rgba } from 'polished'; const StyledWrapper = styled.div` /* Main container */ .theme-menu { min-width: 200px; height: 325px; padding: 8px; background: ${(props) => props.theme.dropdown.bg}; border: 1px solid ${(props) => props.theme.dropdown.border}; border-radius: 6px; box-shadow: ${(props) => props.theme.dropdown.shadow}; outline: none; &.two-columns { min-width: 400px; } } /* Mode section */ .mode-section { padding: 0 8px 12px 8px; margin: 0 -8px; border-bottom: 1px solid ${(props) => props.theme.dropdown.separator}; } .mode-label { font-size: 12px; color: ${(props) => props.theme.dropdown.mutedText}; margin-bottom: 8px; } .mode-buttons { display: flex; gap: 10px; } .mode-button { display: flex; align-items: center; justify-content: center; width: 24px; height: 24px; padding: 8px 4px; border: 1px solid ${(props) => props.theme.dropdown.separator}; border-radius: 4px; background: transparent; color: ${(props) => props.theme.dropdown.mutedText}; cursor: pointer; transition: all 0.15s ease; &:hover { background: ${(props) => props.theme.dropdown.hoverBg}; color: ${(props) => props.theme.dropdown.color}; } &.focused { background: ${(props) => props.theme.dropdown.hoverBg}; color: ${(props) => props.theme.dropdown.color}; outline: none; } &.active { background: ${(props) => rgba(props.theme.dropdown.selectedColor, 0.08)}; border-color: ${(props) => props.theme.dropdown.selectedColor}; color: ${(props) => props.theme.dropdown.selectedColor}; &.focused { background: ${(props) => rgba(props.theme.dropdown.selectedColor, 0.15)}; outline: none; } } } /* Theme lists container */ .theme-lists { display: flex; gap: 24px; &.two-columns { gap: 0; .theme-list { flex: 1; padding: 8px 0; &:first-child { padding-right: 12px; border-right: 1px solid ${(props) => props.theme.dropdown.separator}; } &:last-child { padding-left: 12px; } } } } /* Individual theme list */ .theme-list { min-width: 180px; padding-top: 8px; } .theme-list-label { display: flex; align-items: center; gap: 8px; font-size: 12px; color: ${(props) => props.theme.dropdown.mutedText}; margin-bottom: 8px; } .active-badge { font-size: 10px; font-weight: 500; padding: 2px 6px; border-radius: 4px; background: ${(props) => rgba(props.theme.dropdown.selectedColor, 0.12)}; color: ${(props) => props.theme.dropdown.selectedColor}; } /* Theme item */ .theme-item { display: flex; align-items: center; justify-content: space-between; height: 26px; padding: 4px 8px; border-radius: 4px; cursor: pointer; outline: none; color: ${(props) => props.theme.dropdown.color}; font-size: ${(props) => props.theme.font.size.sm}; &:hover, &.focused { background: ${(props) => props.theme.dropdown.hoverBg}; } &.active { color: ${(props) => props.theme.dropdown.selectedColor}; background: ${(props) => rgba(props.theme.dropdown.selectedColor, 0.08)}; &.focused { background: ${(props) => rgba(props.theme.dropdown.selectedColor, 0.15)}; } } } .theme-item-label { flex: 1; white-space: nowrap; } .check-icon { flex-shrink: 0; margin-left: 12px; color: ${(props) => props.theme.dropdown.selectedColor}; } `; export default StyledWrapper; ================================================ FILE: packages/bruno-app/src/components/StatusBar/ThemeDropdown/index.js ================================================ import React, { useState, useRef, useCallback, useEffect } from 'react'; import Tippy from '@tippyjs/react'; import { IconCheck, IconSun, IconMoon, IconDeviceDesktop } from '@tabler/icons'; import ToolHint from 'components/ToolHint'; import { useTheme } from 'providers/Theme'; import { getLightThemes, getDarkThemes } from 'themes/index'; import StyledWrapper from './StyledWrapper'; // Constants const MODES = ['light', 'dark', 'system']; const MODE_BUTTONS = [ { mode: 'light', icon: IconSun, title: 'Light' }, { mode: 'dark', icon: IconMoon, title: 'Dark' }, { mode: 'system', icon: IconDeviceDesktop, title: 'System' } ]; const ThemeDropdown = ({ children }) => { // Dropdown state const [isOpen, setIsOpen] = useState(false); const [tooltipEnabled, setTooltipEnabled] = useState(true); // Keyboard navigation state const [focusedSection, setFocusedSection] = useState('mode'); const [focusedIndex, setFocusedIndex] = useState(0); const [isKeyboardNav, setIsKeyboardNav] = useState(false); // Refs for focus management const menuRef = useRef(null); const modeButtonRefs = useRef([]); const lightItemRefs = useRef([]); const darkItemRefs = useRef([]); // Theme context const { storedTheme, setStoredTheme, displayedTheme, themeVariantLight, themeVariantDark, setThemeVariantLight, setThemeVariantDark } = useTheme(); // Theme data const lightThemes = getLightThemes(); const darkThemes = getDarkThemes(); const isSystemMode = storedTheme === 'system'; // Helper to get class names for focusable items const getFocusedClass = (section, index) => { return isKeyboardNav && focusedSection === section && focusedIndex === index ? 'focused' : ''; }; // Handlers const handleModeSelect = (mode) => setStoredTheme(mode); const handleThemeSelect = (themeId, isLight) => { if (isLight) { setThemeVariantLight(themeId); } else { setThemeVariantDark(themeId); } }; const handleOpen = () => { setTooltipEnabled(false); setIsOpen(true); setFocusedSection('mode'); setFocusedIndex(0); setIsKeyboardNav(false); }; const handleClose = () => { setIsOpen(false); setTimeout(() => setTooltipEnabled(true), 100); }; const handleMouseEnter = (section, index) => { setIsKeyboardNav(false); setFocusedSection(section); setFocusedIndex(index); }; // Get available sections based on current mode const getAvailableSections = useCallback(() => { if (isSystemMode) return ['mode', 'light', 'dark']; return storedTheme === 'light' ? ['mode', 'light'] : ['mode', 'dark']; }, [isSystemMode, storedTheme]); // Get max index for a section const getMaxIndex = useCallback((section) => { switch (section) { case 'mode': return 2; case 'light': return lightThemes.length - 1; case 'dark': return darkThemes.length - 1; default: return 0; } }, [lightThemes.length, darkThemes.length]); // Get mode index for returning to mode section const getModeIndex = useCallback(() => { return MODES.indexOf(storedTheme); }, [storedTheme]); // Focus element based on current section and index useEffect(() => { if (!isOpen) return; const timer = setTimeout(() => { const refs = { mode: modeButtonRefs, light: lightItemRefs, dark: darkItemRefs }; refs[focusedSection]?.current[focusedIndex]?.focus(); }, 0); return () => clearTimeout(timer); }, [isOpen, focusedSection, focusedIndex]); // Keyboard navigation handler const handleKeyDown = useCallback((e) => { if (!isOpen) return; const sections = getAvailableSections(); const maxIndex = getMaxIndex(focusedSection); const navigationHandlers = { 'Escape': () => { e.preventDefault(); handleClose(); }, 'ArrowDown': () => { e.preventDefault(); setIsKeyboardNav(true); if (focusedSection === 'mode') { setFocusedSection(sections[1]); setFocusedIndex(0); } else if (focusedIndex < maxIndex) { setFocusedIndex(focusedIndex + 1); } }, 'ArrowUp': () => { e.preventDefault(); setIsKeyboardNav(true); if (focusedSection !== 'mode') { if (focusedIndex > 0) { setFocusedIndex(focusedIndex - 1); } else { setFocusedSection('mode'); setFocusedIndex(getModeIndex()); } } }, 'ArrowLeft': () => { e.preventDefault(); setIsKeyboardNav(true); if (focusedSection === 'mode') { if (focusedIndex > 0) setFocusedIndex(focusedIndex - 1); } else if (isSystemMode && focusedSection === 'dark') { setFocusedSection('light'); setFocusedIndex(Math.min(focusedIndex, lightThemes.length - 1)); } }, 'ArrowRight': () => { e.preventDefault(); setIsKeyboardNav(true); if (focusedSection === 'mode') { if (focusedIndex < 2) setFocusedIndex(focusedIndex + 1); } else if (isSystemMode && focusedSection === 'light') { setFocusedSection('dark'); setFocusedIndex(Math.min(focusedIndex, darkThemes.length - 1)); } }, 'Enter': () => { e.preventDefault(); if (focusedSection === 'mode') { handleModeSelect(MODES[focusedIndex]); } else if (focusedSection === 'light') { handleThemeSelect(lightThemes[focusedIndex].id, true); } else if (focusedSection === 'dark') { handleThemeSelect(darkThemes[focusedIndex].id, false); } }, ' ': () => navigationHandlers.Enter(), 'Tab': () => handleClose() }; navigationHandlers[e.key]?.(); }, [ isOpen, focusedSection, focusedIndex, getAvailableSections, getMaxIndex, getModeIndex, isSystemMode, lightThemes, darkThemes ]); // Set up keyboard listener useEffect(() => { if (!isOpen) return; document.addEventListener('keydown', handleKeyDown); return () => document.removeEventListener('keydown', handleKeyDown); }, [isOpen, handleKeyDown]); // Render theme list const renderThemeList = (themes, isLight, currentVariant, label) => { const refs = isLight ? lightItemRefs : darkItemRefs; const section = isLight ? 'light' : 'dark'; const isActiveSystemTheme = isSystemMode && ((isLight && displayedTheme === 'light') || (!isLight && displayedTheme === 'dark')); return (
    {label} {isActiveSystemTheme && Active}
    {themes.map((theme, index) => { const isActive = currentVariant === theme.id; return (
    (refs.current[index] = el)} className={`theme-item ${isActive ? 'active' : ''} ${getFocusedClass(section, index)}`} role="option" aria-selected={isActive} tabIndex={-1} onClick={() => handleThemeSelect(theme.id, isLight)} onMouseEnter={() => handleMouseEnter(section, index)} > {theme.name} {isActive && }
    ); })}
    ); }; // Render mode buttons const renderModeButtons = () => (
    {MODE_BUTTONS.map((btn, index) => { const Icon = btn.icon; const isActive = storedTheme === btn.mode; return ( ); })}
    ); // Menu content const menuContent = (
    Appearance
    {renderModeButtons()}
    {(storedTheme === 'light' || isSystemMode) && renderThemeList(lightThemes, true, themeVariantLight, 'Light theme')} {(storedTheme === 'dark' || isSystemMode) && renderThemeList(darkThemes, false, themeVariantDark, 'Dark theme')}
    ); return ( ); }; export default ThemeDropdown; ================================================ FILE: packages/bruno-app/src/components/StatusBar/index.js ================================================ import React, { useState } from 'react'; import { useSelector, useDispatch } from 'react-redux'; import find from 'lodash/find'; import { IconSettings, IconCookie, IconTool, IconSearch, IconPalette, IconBrandGithub } from '@tabler/icons'; import Mousetrap from 'mousetrap'; import { getKeyBindingsForActionAllOS } from 'providers/Hotkeys/keyMappings'; import ToolHint from 'components/ToolHint'; import Cookies from 'components/Cookies'; import Notifications from 'components/Notifications'; import Portal from 'components/Portal'; import ThemeDropdown from './ThemeDropdown'; import { openConsole } from 'providers/ReduxStore/slices/logs'; import { addTab } from 'providers/ReduxStore/slices/tabs'; import { useApp } from 'providers/App'; import StyledWrapper from './StyledWrapper'; const StatusBar = () => { const dispatch = useDispatch(); const activeWorkspaceUid = useSelector((state) => state.workspaces.activeWorkspaceUid); const workspaces = useSelector((state) => state.workspaces.workspaces); const showHomePage = useSelector((state) => state.app.showHomePage); const showManageWorkspacePage = useSelector((state) => state.app.showManageWorkspacePage); const showApiSpecPage = useSelector((state) => state.app.showApiSpecPage); const tabs = useSelector((state) => state.tabs.tabs); const activeTabUid = useSelector((state) => state.tabs.activeTabUid); const activeTab = find(tabs, (t) => t.uid === activeTabUid); const logs = useSelector((state) => state.logs.logs); const [cookiesOpen, setCookiesOpen] = useState(false); const { version } = useApp(); const activeWorkspace = workspaces.find((w) => w.uid === activeWorkspaceUid); const errorCount = logs.filter((log) => log.type === 'error').length; const handleConsoleClick = () => { dispatch(openConsole()); }; const handlePreferencesClick = () => { const collectionUid = activeTab?.collectionUid || activeWorkspace?.scratchCollectionUid; dispatch( addTab({ type: 'preferences', uid: collectionUid ? `${collectionUid}-preferences` : 'preferences', collectionUid: collectionUid }) ); }; const openGlobalSearch = () => { const bindings = getKeyBindingsForActionAllOS('globalSearch') || []; bindings.forEach((binding) => { Mousetrap.trigger(binding); }); }; return ( {cookiesOpen && ( { setCookiesOpen(false); document.querySelector('[data-trigger="cookies"]').focus(); }} aria-modal="true" role="dialog" aria-labelledby="cookies-title" aria-describedby="cookies-description" /> )}
    v{version}
    ); }; export default StatusBar; ================================================ FILE: packages/bruno-app/src/components/StatusDot/index.js ================================================ import React from 'react'; import DotIcon from 'components/Icons/Dot'; const StatusDot = ({ type = 'default' }) => ( ); export default StatusDot; ================================================ FILE: packages/bruno-app/src/components/StopWatch/index.js ================================================ import React, { useState, useEffect } from 'react'; const StopWatch = ({ startTime }) => { const [currentTime, setCurrentTime] = useState(Date.now()); useEffect(() => { if (!startTime) return; const intervalId = setInterval(() => { setCurrentTime(Date.now()); }, 100); return () => clearInterval(intervalId); }, [startTime]); if (!startTime) return Loading...; const elapsedTime = currentTime - startTime; if (elapsedTime < 250) return Loading...; const seconds = elapsedTime / 1000; return {seconds.toFixed(1)}s; }; export default React.memo(StopWatch); ================================================ FILE: packages/bruno-app/src/components/Tab/index.js ================================================ import React from 'react'; import classnames from 'classnames'; const Tab = ({ name, label, isActive, onClick, count = 0, className = '', ...props }) => { const tabClassName = classnames('tab select-none', { active: isActive }, className); return (
    onClick(name)} data-testid={`tab-${name}`} {...props} > {label} {count > 0 && {count}}
    ); }; export default Tab; ================================================ FILE: packages/bruno-app/src/components/Table/StyledWrapper.js ================================================ import styled from 'styled-components'; const StyledWrapper = styled.div` table { width: 100%; display: grid; overflow-y: hidden; overflow-x: auto; padding: 0 1.5px; // for icon hover position: inherit; grid-template-columns: ${({ columns }) => columns?.[0]?.width ? columns.map((col) => `${col?.width}`).join(' ') : columns.map((col) => `${100 / columns.length}%`).join(' ')}; } table thead, table tbody, table tr { display: contents; } table th { position: relative; font-weight: 400; border-bottom: 1px solid ${(props) => props.theme.table.border}; } table tr td { padding: 0.5rem; text-align: left; border-top: 1px solid ${(props) => props.theme.table.border}; border-right: 1px solid ${(props) => props.theme.table.border}; } tr { transition: transform 0.2s ease-in-out; } tr.dragging { opacity: 0.5; } tr.hovered { transform: translateY(10px); /* Adjust the value as needed for the animation effect */ } table tr th { padding: 0.5rem; text-align: left; border-top: 1px solid ${(props) => props.theme.table.border}; border-right: 1px solid ${(props) => props.theme.table.border}; &:nth-child(1) { border-left: 1px solid ${(props) => props.theme.table.border}; } } `; export default StyledWrapper; ================================================ FILE: packages/bruno-app/src/components/Table/index.js ================================================ import { useState, useRef, useEffect, useCallback } from 'react'; import StyledWrapper from './StyledWrapper'; const Table = ({ minColumnWidth = 1, headers = [], children }) => { const [activeColumnIndex, setActiveColumnIndex] = useState(null); const tableRef = useRef(null); const columns = headers?.map((item) => ({ ...item, ref: useRef() })); const updateDivHeights = () => { if (tableRef.current) { const height = tableRef.current.offsetHeight; columns.forEach((col) => { if (col.ref.current) { col.ref.current.querySelector('.resizer').style.height = `${height}px`; } }); } }; useEffect(() => { updateDivHeights(); window.addEventListener('resize', updateDivHeights); return () => { window.removeEventListener('resize', updateDivHeights); }; }, [columns]); useEffect(() => { if (tableRef.current) { const observer = new MutationObserver(updateDivHeights); observer.observe(tableRef.current, { childList: true, subtree: true }); return () => { observer.disconnect(); }; } }, [columns]); const handleMouseDown = (index) => (e) => { setActiveColumnIndex(index); }; const handleMouseMove = useCallback( (e) => { const gridColumns = columns.map((col, i) => { if (i === activeColumnIndex) { const width = e.clientX - col.ref?.current?.getBoundingClientRect()?.left; if (width >= minColumnWidth) { return `${width}px`; } } return `${col.ref.current.offsetWidth}px`; }); tableRef.current.style.gridTemplateColumns = `${gridColumns.join(' ')}`; }, [activeColumnIndex, columns, minColumnWidth] ); const removeListeners = useCallback(() => { window.removeEventListener('mousemove', handleMouseMove); window.removeEventListener('mouseup', removeListeners); }, [handleMouseMove]); const handleMouseUp = useCallback(() => { setActiveColumnIndex(null); removeListeners?.(); }, [removeListeners]); useEffect(() => { if (activeColumnIndex !== null) { window.addEventListener('mousemove', handleMouseMove); window.addEventListener('mouseup', handleMouseUp); } return () => { removeListeners(); }; }, [activeColumnIndex, handleMouseMove, handleMouseUp, removeListeners]); return (
    {columns.map(({ ref, name }, i) => ( ))} {children}
    {name}
    ); }; export default Table; ================================================ FILE: packages/bruno-app/src/components/Tabs/StyledWrapper.js ================================================ import styled from 'styled-components'; const StyledWrapper = styled.div` .tabs-list { display: inline-flex; height: 2rem; width: fit-content; justify-content: center; gap: 0.25rem; } .tab-trigger { display: inline-flex; align-items: center; justify-content: center; border-radius: 4px; padding: 8px; font-size: 0.75rem; white-space: nowrap; cursor: pointer; border: 1px solid transparent; background: transparent; color: ${(props) => props.theme.tabs.secondary.inactive.color}; transition: all 0.15s ease; &:hover { background: ${(props) => props.theme.tabs.secondary.inactive.bg}; } &.active { background: ${(props) => props.theme.tabs.secondary.active.bg}; color: ${(props) => props.theme.tabs.secondary.active.color}; } } `; export default StyledWrapper; ================================================ FILE: packages/bruno-app/src/components/Tabs/index.js ================================================ import React, { createContext, useContext } from 'react'; import classnames from 'classnames'; import StyledWrapper from './StyledWrapper'; const TabsContext = createContext(); export const Tabs = ({ value, onValueChange, children, className = '' }) => { return ( {children} ); }; export const TabsList = ({ children, className = '' }) => { return
    {children}
    ; }; export const TabsTrigger = ({ value: triggerValue, children, className = '' }) => { const { value, onValueChange } = useContext(TabsContext); const isActive = value === triggerValue; return ( ); }; export const TabsContent = ({ value: contentValue, children, className = '', dataTestId = '' }) => { const { value } = useContext(TabsContext); const isActive = value === contentValue; return (
    {children}
    ); }; ================================================ FILE: packages/bruno-app/src/components/TagList/StyledWrapper.js ================================================ import styled from 'styled-components'; const StyledWrapper = styled.div` .tags-container { display: flex; flex-wrap: wrap; gap: 8px; min-height: 40px; padding: 8px 0; } .tag-item { display: inline-flex; align-items: center; gap: 6px; padding: 3px 7px; background-color: ${(props) => props.theme.sidebar.bg}; border: 1px solid ${(props) => props.theme.requestTabs.bottomBorder}; border-radius: 3px; font-size: ${(props) => props.theme.font.size.sm}; font-weight: 500; color: ${(props) => props.theme.text}; max-width: 200px; transition: all 0.2s ease; box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); cursor: default; &:has(.tag-remove:hover) { background-color: ${(props) => props.theme.background.surface2}; border-color: ${(props) => props.theme.requestTabs.bottomBorder}; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); transform: translateY(-1px); } .tag-remove { cursor: pointer; } } .tag-icon { color: ${(props) => props.theme.textSecondary || props.theme.text}; opacity: 0.7; flex-shrink: 0; } .tag-text { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; } .tag-remove { display: flex; align-items: center; justify-content: center; background: none; border: none; cursor: pointer; padding: 2px; border-radius: 3px; color: ${(props) => props.theme.textSecondary || props.theme.text}; transition: all 0.2s ease; flex-shrink: 0; opacity: 0.7; &:hover { background-color: ${(props) => props.theme.danger}; color: white; opacity: 1; transform: scale(1.1); } &:focus-visible { outline: 2px solid ${(props) => props.theme.danger}; outline-offset: 1px; } } .empty-state { display: flex; align-items: center; gap: 12px; padding: 24px 16px; background-color: ${(props) => props.theme.sidebar.bg}; border: 2px dashed ${(props) => props.theme.requestTabs.bottomBorder}; border-radius: 3px; color: ${(props) => props.theme.textSecondary || props.theme.text}; text-align: left; } .empty-icon { opacity: 0.5; flex-shrink: 0; } .empty-text { flex: 1; min-width: 0; } .empty-title { font-weight: 500; margin: 0 0 4px 0; font-size: ${(props) => props.theme.font.size.base}; color: ${(props) => props.theme.text}; } .empty-subtitle { margin: 0; font-size: ${(props) => props.theme.font.size.sm}; opacity: 0.8; line-height: 1.5; color: ${(props) => props.theme.textSecondary || props.theme.text}; } /* Responsive design */ @media (max-width: 480px) { .tags-container { gap: 6px; } .tag-item { padding: 4px 8px; font-size: ${(props) => props.theme.font.size.xs}; } .empty-state { padding: 16px 12px; flex-direction: column; text-align: center; } } `; export default StyledWrapper; ================================================ FILE: packages/bruno-app/src/components/TagList/index.js ================================================ import { useState } from 'react'; import { IconX, IconTag } from '@tabler/icons'; import StyledWrapper from './StyledWrapper'; import SingleLineEditor from 'components/SingleLineEditor/index'; import { useTheme } from 'providers/Theme/index'; const TagList = ({ tagsHintList = [], handleAddTag, tags, handleRemoveTag, onSave, handleValidation, collectionFormat }) => { const { displayedTheme } = useTheme(); const isBruFormat = collectionFormat === 'bru'; const tagNameRegex = isBruFormat ? /^[\p{L}\p{N}_-]+$/u : /^[\p{L}\p{N}_-](?:[\p{L}\p{N}_\s-]*[\p{L}\p{N}_-])?$/u; const [text, setText] = useState(''); const [error, setError] = useState(''); const handleInputChange = (value) => { setError(''); setText(value); }; const handleKeyDown = (e) => { if (!text.trim()) { return; } if (!tagNameRegex.test(text)) { setError(isBruFormat ? 'Tags in BRU format must only contain letters, numbers, "-", "_".' : 'Tags must only contain letters, numbers, spaces, "-", "_"' ); return; } if (tags.includes(text)) { setError(`Tag "${text}" already exists`); return; } if (handleValidation) { const error = handleValidation(text); if (error) { setError(error); return; } } handleAddTag(text); setText(''); }; return ( {error && {error}}
      {tags && tags.length ? tags.map((_tag) => (
    • )) : null}
    ); }; export default TagList; ================================================ FILE: packages/bruno-app/src/components/ToggleSwitch/StyledWrapper.js ================================================ import styled from 'styled-components'; const switchSizes = { '2xs': { width: 32, height: 16, buttonSize: 14 }, 'xs': { width: 40, height: 20, buttonSize: 18 }, 's': { width: 44, height: 22, buttonSize: 20 }, 'm': { width: 50, height: 24, buttonSize: 22 }, // default size 'l': { width: 56, height: 28, buttonSize: 26 }, 'xl': { width: 64, height: 32, buttonSize: 30 }, '2xl': { width: 72, height: 36, buttonSize: 34 } }; const getSizeValues = (size = 'm') => switchSizes[size] || switchSizes.m; export const Switch = styled.div` position: relative; display: inline-block; width: ${(props) => getSizeValues(props.size).width}px; height: ${(props) => getSizeValues(props.size).height}px; border-radius: ${(props) => getSizeValues(props.size).height}px; `; export const Checkbox = styled.input` opacity: 0; width: 0; height: 0; &:checked + label div { background-color: ${(props) => props.activeColor || props.theme.primary.solid}; } &:checked + label div:before { transform: translateX(${(props) => getSizeValues(props.size).width - getSizeValues(props.size).buttonSize - 2}px); } `; export const Label = styled.label` position: absolute; top: 0; left: 0; width: 100%; height: 100%; cursor: pointer; background-color: ${(props) => props.theme.input.bg}; border-radius: 24px; div { position: absolute; top: 0; left: 0; width: 100%; height: 100%; background-color: ${(props) => props.theme.colors.text.muted}; border-radius: 24px; transition: transform 0.2s; } `; export const Inner = styled.div` position: absolute; top: 2px; left: 2px; right: 2px; bottom: 2px; background-color: #fafafa; transition: 0.4s; border-radius: ${(props) => getSizeValues(props.size).height - 2}px; `; export const SwitchButton = styled.div` position: absolute; height: ${(props) => getSizeValues(props.size).buttonSize}px; width: ${(props) => getSizeValues(props.size).buttonSize}px; left: 2px; bottom: 2px; background-color: white; transition: 0.4s; border-radius: 50%; &:before { content: ''; position: absolute; height: ${(props) => getSizeValues(props.size).buttonSize - 2}px; width: ${(props) => getSizeValues(props.size).buttonSize - 2}px; background-color: white; top: 2px; left: 2px; transition: 0.4s; border-radius: 50%; } `; ================================================ FILE: packages/bruno-app/src/components/ToggleSwitch/index.js ================================================ import { Checkbox, Inner, Label, Switch, SwitchButton } from './StyledWrapper'; const ToggleSwitch = ({ isOn, handleToggle, size = 'm', activeColor, ...props }) => { return ( {}} /> ); }; export default ToggleSwitch; ================================================ FILE: packages/bruno-app/src/components/ToolHint/index.js ================================================ import React from 'react'; import { Tooltip as ReactToolHint } from 'react-tooltip'; import { useTheme } from 'providers/Theme'; const ToolHint = ({ text, toolhintId, anchorSelect, children, tooltipStyle = {}, place = 'top', hidden = false, offset, positionStrategy, theme = null, className = '', delayShow = 200 }) => { const { theme: contextTheme } = useTheme(); const appliedTheme = theme || contextTheme; const toolhintBackgroundColor = appliedTheme?.background.surface0; const toolhintTextColor = appliedTheme?.text; const combinedToolhintStyle = { ...tooltipStyle, fontSize: '0.75rem', padding: '0.25rem 0.5rem', zIndex: 9999, backgroundColor: toolhintBackgroundColor, color: toolhintTextColor }; const toolhintProps_final = anchorSelect ? { anchorSelect } : { anchorId: toolhintId }; return ( <> {!anchorSelect && {children}} {anchorSelect && children}