{"owner":"usebruno","repo":"bruno","hasSkills":true,"totalSkillsCount":45,"totalTokensCount":36879,"categories":["claude-rule","anthropic-skill","plugin-manifest"],"hasMcp":false,"mcpConfig":null,"found":[".claude/CLAUDE.md",".claude/rules/architecture.md",".claude/rules/bruno-app-layout.md",".claude/rules/conventions.md",".claude/rules/cross-platform.md",".claude/rules/dsl-changes.md",".claude/rules/electron-ipc.md",".claude/rules/feature-parity.md",".claude/rules/redux-store.md",".claude/rules/sqlite.md",".claude/rules/testing.md",".claude/skills/code-review/SKILL.md",".claude/skills/code-review/reviewers/_contract.md",".claude/skills/code-review/reviewers/architecture.md",".claude/skills/code-review/reviewers/conventions.md",".claude/skills/code-review/reviewers/correctness.md",".claude/skills/code-review/reviewers/cross-platform.md",".claude/skills/code-review/reviewers/dsl-changes.md",".claude/skills/code-review/reviewers/e2e-tests.md",".claude/skills/code-review/reviewers/feature-parity.md",".claude/skills/code-review/reviewers/react.md",".claude/skills/code-review/reviewers/security.md",".claude/skills/write-e2e-test/SKILL.md","packages/bruno-app/storybook/README.md","packages/bruno-cli/docker/README.md","packages/bruno-cli/docker/images/alpine/README.md","packages/bruno-cli/docker/images/debian/README.md","packages/bruno-cli/readme.md","packages/bruno-common/readme.md","packages/bruno-converters/readme.md","packages/bruno-converters/tests/postman/round-trip/README.md","packages/bruno-docs/readme.md","packages/bruno-electron/readme.md","packages/bruno-filestore/README.md","packages/bruno-graphql-docs/readme.md","packages/bruno-js/readme.md","packages/bruno-lang/readme.md","packages/bruno-query/readme.md","packages/bruno-schema/readme.md","packages/bruno-sqlite/README.md","packages/bruno-tests/collection/readme.md","packages/bruno-tests/collection_level_oauth2/readme.md","packages/bruno-tests/collection_oauth2/readme.md","packages/bruno-tests/readme.md","packages/bruno-tests/reports/README.md"],"skills":{".claude/CLAUDE.md":"# Project Guide\n\nBruno fork — open-source API client (Electron + React + Redux).\n\nBruno is an API **client**, not a server: it sends requests and handles responses (auth, params, headers, bodies, environments, cookies). Judge every behavior and edge case by \"what should a *client* do here?\" — surface malformed or hostile responses clearly and stay robust; don't enforce server-side correctness.\n\n## Quick Commands\n\n```bash\nnpm run dev              # Run electron + react concurrently\nnpm run dev:watch        # Same, with hot-reload of the electron main process\nnpm run storybook        # Component dev (bruno-app)\nnpm run lint:fix         # ESLint fix\nnpm test --workspaces    # Unit tests (Jest) across all packages\n```\n\nFull script list: root `package.json`. Setup: `nvm use && npm i --legacy-peer-deps && npm run setup` (Node **v22.12.0**, see `.nvmrc`).\n\n### Running a single test\n\n```bash\n# Single unit test (Jest) — scope to the workspace, pass a path/pattern after --\nnpm test --workspace=packages/bruno-app -- path/to/file.spec.js\nnpm test --workspace=packages/bruno-requests -- -t \"test name pattern\"\n\n# Single e2e test (Playwright) — a project flag is REQUIRED (config has no default)\nnpx playwright test tests/collection/create-collection.spec.ts --project=default\nnpx playwright test --project=default -g \"test name pattern\"\n```\n\nPrefer the smallest scope — one workspace, one spec — over `--workspaces` or the full e2e run.\n\n### Building shared packages\n\n`npm run dev` does **not** rebuild the shared packages (bruno-common, bruno-requests,\nbruno-filestore, bruno-sqlite, bruno-converters, bruno-query, bruno-schema-types, bruno-graphql-docs).\nAfter editing one, rebuild it or run its watcher, or the app won't pick up changes:\n\n```bash\nnpm run build:bruno-common      # also :bruno-requests, :bruno-filestore, :bruno-sqlite, etc.\nnpm run watch:common            # also watch:requests, watch:converters\n```\n\n## Key Architecture\n\n- **Main process entry**: `packages/bruno-electron/src/index.js`\n- **Renderer + Redux store**: `packages/bruno-app/src/providers/ReduxStore/`\n- Packages live under `packages/`, with top-level `tests/` and `playwright/`.\n\n## Testing\n\n- **Unit**: Jest, config per-package (`packages/*/jest.config.js`); bruno-app uses jsdom + `jest.setup.js`.\n- **E2E**: Playwright — fixtures, helpers, isolation rules, and pitfalls live in\n  `.claude/rules/testing.md`; use the `write-e2e-test` skill when adding specs.\n\n## Coding Standards\n\nFull list: `CODING_STANDARDS.md`. Mechanical style (indent, quotes, semicolons, no trailing\ncommas, arrow-param parens) is ESLint-enforced — run `npm run lint:fix`, don't hand-police it.\nNon-obvious project rules worth holding:\n\n- React: avoid `useEffect` (prefer derived state / custom hooks); import hooks by name, never `React.useX`; a component is controlled XOR uncontrolled; Tailwind for layout, styled-components `theme` for colors.\n- Every component owns a directory — `<ComponentName>/index.js(x)`, never `<ComponentName>.js(x)`.\n- Extract for readability or clear reuse; avoid only indirection that adds no payoff. Add `data-testid` attributes for Playwright selectors.\n\n## Detailed rules & references\n\nPath-scoped rules in `.claude/rules/` auto-attach when you touch matching files:\n`architecture.md` (`@usebruno/*` dependency boundaries, manifests), `bruno-app-layout.md` (bruno-app\nmodule layout), `electron-ipc.md` (IPC handlers + startup), `redux-store.md` (slices/middleware),\n`sqlite.md` (`@usebruno/sqlite` — migrations, statements, per-statement IPC), `testing.md` (e2e\npatterns & gotchas), `cross-platform.md` (Windows file/process/path pitfalls), `dsl-changes.md`\n(on-disk `.bru`/`.yml` format & backward compat), `feature-parity.md` (co-related features —\napp↔CLI twins, converter/shim/level/protocol pairs), `conventions.md` (readability, reuse,\nreplacement & pre-submit hygiene).\n\nRead on demand (not auto-loaded): `.claude/reference/architecture.md` — the monorepo map, request\npipeline, sandbox, core types, and dependency versions. Consult it before cross-package or\narchitectural work.\n",".claude/rules/architecture.md":"---\npaths:\n  - \"packages/**/*\"\n---\n\n# Dependency Boundaries\n\nInternal `@usebruno/*` dependencies (from each `package.json`) form a strict DAG. **New code must\nrespect it** — a cyclic or upward dependency is an architectural bug, not a convenience.\n\nThe full monorepo map (build tools, request pipeline, sandbox, file formats, core data-model\ntypes, dependency versions) is the on-demand reference `.claude/reference/architecture.md` — read\nit before non-trivial cross-package or architectural work.\n\n## Dependency direction & ownership boundaries\n\n- **Leaf libs — zero internal `@usebruno/*` deps:** bruno-common, bruno-lang, bruno-query,\n  bruno-requests, bruno-graphql-docs, bruno-schema, bruno-schema-types, bruno-toml, bruno-sqlite\n  (a leaf, but *not* platform-neutral — see guardrail 6).\n- **Mid consumers:** bruno-js → (common, query); bruno-converters → (common, schema; schema-types\n  as devDep); bruno-filestore → (common, lang; schema-types as devDep).\n- **Top consumers (things flow *into* them, never out):** bruno-cli → (common, converters,\n  filestore, js, lang, requests); bruno-electron → (common, converters, filestore, js, lang,\n  requests, schema, sqlite); bruno-app → (common, converters, graphql-docs, schema).\n\nGuardrails this enforces:\n\n1. **bruno-common is the browser-safe base leaf.** It runs in the web renderer (`bruno-app`), not\n   just Node, so it must stay platform-neutral: no Node built-ins (`fs`, `path`, `os`, `crypto`,\n   `child_process`, `node:*`) and no dependency that itself pulls in Node. It currently ships with\n   zero runtime dependencies — keep it that way. It also depends on no other `@usebruno/*` package;\n   a util that needs Node or another bruno package belongs in a different package.\n2. **No shared/library package may import bruno-app or bruno-electron.** Renderer- or\n   Electron-specific code must not be pushed down into a lib to make it importable upward.\n3. **bruno-js must stay Electron-free.** It runs in both the Electron main process *and* the CLI\n   (bruno-cli → js). Adding an `electron`/IPC import to bruno-js breaks the CLI. Sandbox logic\n   belongs in bruno-js; host wiring belongs in bruno-electron.\n4. **bruno-schema-types is types-only** (a *devDependency* of converters/filestore, built with\n   plain `tsc`). Import types from it; never add runtime code or a runtime import of it.\n5. **bruno-schema (Yup) and bruno-schema-types (TS types) are distinct and both live.** app +\n   converters use `@usebruno/schema` for runtime validation; filestore + converters use\n   `@usebruno/schema-types` for compile-time types. A data-model change usually touches both.\n6. **bruno-sqlite is main-process only.** `@usebruno/sqlite` (= `/node`) imports `node:sqlite`,\n   `node:fs` and `node:crypto`; importing it from the renderer breaks the bundle. The renderer\n   reaches statements only through the per-statement IPC handlers in bruno-electron. See\n   `.claude/rules/sqlite.md`.\n\n## Declared dependencies must match real imports\n\nA package's `package.json` is the contract; workspace hoisting hides breaches of it. Declare every\nimport in the manifest of the package that imports it — one resolving only because a sibling or the\nroot happens to pull it in is a latent break. Test- and build-only packages belong in\n`devDependencies`; anything in `dependencies` ships to users with its transitive tree. Drop a\ndeclaration when the change removes its last consumer.\n",".claude/rules/bruno-app-layout.md":"---\npaths:\n  - \"packages/bruno-app/**/*\"\n---\n\n# Module & Directory Layout (bruno-app)\n\nLayout is predictable enough here to be load-bearing, so a misplaced file is an architectural defect,\nnot a style nit — a breach blocks, the same as a dependency-direction violation in\n`.claude/rules/architecture.md`.\n\n- **A component owns a directory: `<ComponentName>/index.js(x)`, never `<ComponentName>.js`** — at\n  every nesting depth, including a sub-component of a component. Same shape for hooks\n  (`src/hooks/useX/index.js`) and `src/ui/` primitives.\n- **Everything a component owns lives in its directory** beside its `index.js` — `StyledWrapper.js`,\n  spec, component-specific utils. Reaching up into a parent's `StyledWrapper.js`, or leaving a\n  component's styles outside its directory, hands ownership of them to no one.\n- **`src/ui/` is a leaf**: app-agnostic presentational primitives only, and it must not import from\n  `src/components/` — the edge runs `components → ui`, never back. A `ui/` module needing one either\n  isn't a primitive, or its dependency belongs in `ui/` too.\n- **`utils/` is for stateless helpers you call.** A module *registered* into something — an editor\n  extension, a plugin, a node view, a keymap — is behavior or configuration, and filing it under\n  `utils/` hides that it's part of a schema or document contract. Give those their own directory.\n\nExisting breaches — bare `PascalCase.js(x)` component files, and `ui/MenuDropdown`'s import of\n`components/Dropdown` — are drift, not a second pattern. Don't extend them, and don't raise an\nuntouched one as a finding.\n",".claude/rules/conventions.md":"---\npaths:\n  - \"packages/**/*\"\n  - \"tests/**/*\"\n  - \"scripts/**/*\"\n---\n\n# Coding Conventions, Readability & Hygiene\n\n`CODING_STANDARDS.md` is the source of truth for Bruno's coding standards — read it. This file is\nthe judgment layer over those standards: the readability and code-hygiene calls a linter can't make.\nCode and comments must read as a natural, permanent part of the project — never as artifacts of the\ntask or session that produced them.\n\n## Style & formatting\n\nThe mechanical style — 2-space indent, single quotes (double for JSX/TSX attributes), semicolons,\nno trailing commas, parenthesized arrow params, spacing around arrows, no space before call parens\n— is enforced by ESLint and auto-fixed by `npm run lint:fix`. These deviations are real but\nlow-value to catch by hand: note them briefly rather than dwelling. Naming and casing that ESLint\ncan't mechanically repair still warrant attention.\n\n## Readability\n\n- **Descriptive names.** Functions and variables carry concise, descriptive names; an unclear or\n  misleading name is worth raising even when the code is otherwise correct.\n- **Reuse before you write.** Before adding a component, hook, or helper, search for the one that\n  already exists — by concept, not by the name you'd have picked — and read the nearest sibling\n  solving the same shape of problem, since its call site shows the intended composition. Reuse is\n  usually a *net deletion* — the bespoke markup and its CSS go. Where the existing primitive is\n  *almost* right, widen it rather than standing up a near-duplicate beside it; two near-identical\n  implementations diverge silently.\n- **Extraction & abstraction.** Extract a helper or shared abstraction whenever it genuinely\n  improves readability or serves a clear, anticipated reuse — this is encouraged and not gated on a\n  minimum number of call sites; suggest it where it would help. Avoid only *unnecessary* abstraction:\n  a layer that adds indirection without improving clarity or earning reuse — a generalized utility\n  built for a single site with no foreseeable second user, or options/config added \"for later.\"\n  Breaking a long, complex function into well-named local helpers for readability is always fine.\n- **Single-line indirection.** A one-line function that only forwards to another — adding a stack\n  frame without adding meaning — should be inlined.\n- **Optional chaining.** `?.` belongs only where the null case is handled right there (a fallback,\n  early return, or guard). Used elsewhere it hides whether a value can genuinely be null and works\n  against TypeScript's guarantees; fix the type or narrow first.\n- **Comments explain the why.** Genuinely complex flow deserves a comment covering the rationale an\n  obvious reading can't; self-explanatory code does not. Full bar in **Comments** below.\n- **Functional, but readable.** Prefer obvious, linear pipelines over deep functional machinery\n  (ADTs, monads) — the code should stay easy for any contributor to follow and extend.\n\n## Comments\n\n- **No situational or prompt-driven comments.** A comment must not reference the change, the task, or\n  the moment it was written. Drop `// added to fix ...`, `// as requested`, `// new logic for X`,\n  `// updated to handle ...`, `// per review`. If the reason genuinely matters, state it as a\n  timeless fact about the code (or link the issue/PR) — not as \"what I just did\".\n- **No obvious comments.** Don't restate what the code already says. `// loop over items` above a\n  loop, `// set the name` above `obj.name = name`, `// return the result` — these add nothing. If the\n  code is self-explanatory, leave it uncommented.\n- **Comment the why, not the what.** Reserve comments for what the code can't show: non-obvious\n  rationale, invariants, edge cases, a workaround and the constraint forcing it, units, or a pointer\n  to a spec/issue. These stay useful long after the change lands.\n- **No scaffolding or narration.** No `// ... existing code ...`, no placeholder or TODO-for-me\n  notes, no changelog or step-by-step narration in comments, no commented-out code left behind.\n\n## Beyond comments\n\n- **Anything added needs a live consumer in the same change.** No options, parameters, or\n  configuration \"for later\"; no payload field no reader destructures, branch for a state the producer\n  can't emit, or ignored parameter. Each is dead on arrival and reads as a contract honored somewhere\n  else — if the consumer is a follow-up, leave it out. Extracting a helper for readability or clear,\n  foreseeable reuse is fine (see **Readability → Extraction & abstraction** above).\n- Match the surrounding code's style and naming so a change is indistinguishable from the existing\n  codebase, not visibly bolted on.\n- Keep diffs minimal — no unrelated reformatting or whitespace churn. A drive-by cleanup worth doing\n  belongs in its own commit, and never in a *different package* than the change is about; interleaved,\n  reviewers can't separate scope creep from load-bearing edits.\n\n## Replacing code leaves nothing behind\n\nHalf a migration reads as a complete one — the leftovers survive review and mislead whoever edits the\nfile next. Closing the loop is part of the change.\n\n- **Follow what you replaced to every reference and remove it there** — the CSS whose selectors the\n  new markup no longer emits, the import whose last use just went, the prop nothing passes. Styles are\n  the easiest to leave behind and the most misleading when left: CSS for a class that renders nothing\n  fails silently and looks intentional.\n- **Don't copy a block to its new home and leave the original.** Where both copies must genuinely\n  exist — separate call sites, or one rule enforced on both sides of a process boundary — name the\n  invariant in a comment at both, because nothing else keeps them in sync.\n- **Reconcile the whole flow, not the entry point.** Reshaping something that crosses a boundary — a\n  payload, a serialized field, a lookup key — means walking to every consumer on the far side.\n  Deleting a control (a sanitizer, a validation or escaping step) is a decision to state, not a side\n  effect of a rewrite.\n\n## Before you call it done\n\n- **Run the affected workspace's tests**, not just the specs you wrote.\n- **Changed a return value or payload shape?** Other specs assert it too — exact-equality assertions\n  elsewhere break on an added key.\n- **Walk the acceptance criteria against the diff one at a time.** Compound criteria are what slip:\n  the half of the sentence you didn't have open never got exercised.\n",".claude/rules/cross-platform.md":"---\npaths:\n  - \"scripts/**/*\"\n  - \"packages/bruno-electron/**/*\"\n---\n\n# Cross-Platform Guidelines\n\nThis is an Electron app targeting macOS, Windows, and Linux. All code touching the filesystem, child processes, signals, or paths must work on all three platforms.\n\n## Filesystem\n\n- `fs.rmSync` with `force: true` only suppresses `ENOENT`, NOT `EPERM`/`EBUSY`. Windows locks files aggressively (antivirus, indexing, open handles). Always use `maxRetries` and `retryDelay` when deleting directories.\n- Use `path.join()` / `path.resolve()` for all paths — never hardcode `/` separators.\n- Windows paths are case-insensitive. Use `normalizePath()` (from `utils/common/path`) when comparing collection/workspace paths.\n- `app.getPath()` returns platform-specific directories (`userData`, `documents`, etc.). Never assume POSIX-style locations.\n- File watchers (chokidar) may emit different event orderings across platforms. Don't depend on specific add/change/unlink sequences.\n\n## Child Processes\n\n- `spawn('npm', [...])` requires `shell: true` on Windows because `npm` is `npm.cmd`.\n- `child.kill()` with `shell: true` only kills the shell wrapper (`cmd.exe`) on Windows, leaving orphaned children. Use `taskkill /pid <pid> /T /F` to kill the entire process tree on Windows.\n- `execSync` / `spawn` commands must avoid Unix-only syntax (`&&` chains work in cmd.exe, but pipes and redirects differ).\n\n## Signals & Shutdown\n\n- `SIGINT` / `SIGTERM` are unreliable on Windows with `shell: true`. Also handle `SIGHUP` as fallback.\n- App shutdown must close all file watchers. Each watcher class implements `closeAllWatchers()`, orchestrated by `closeAllWatchers()` in `index.js`.\n\n## Line Endings\n\n- Files authored on Windows use CRLF. When parsing multiline `.bru`/text blocks line by line, split with a CRLF-aware regex (`/\\r\\n|\\r|\\n/`), never on `\\n` alone — a trailing `\\r` otherwise leaks into parsed values and causes spurious dirty-state and diffs. the `.split(/\\r\\n|\\r|\\n/)` in `bruno-lang`'s `v2/src/envToJson.js` is the reference pattern; keep new parsers consistent with it.\n\n## stdout vs stderr\n\n- Dev tools (rsbuild, webpack, electron-builder) may route startup output to stderr on Windows. When detecting patterns in process output, check both streams.\n\n## Platform-Specific Dependencies\n\n- `forceInstallPlatformDeps()` in `scripts/setup.js` installs platform-specific native modules (e.g., `@lydell/node-pty-{platform}-{arch}`).\n- Electron builder config handles platform-specific packaging (`mac`/`win`/`linux` targets).\n\n## Path Separators in Collection/Workspace Stores\n\n- electron-store persists paths as-is. A workspace opened on macOS stores `/Users/...`, on Windows `C:\\Users\\...`.\n- `ELECTRON_USER_DATA_PATH` only applies when `isDev` is true (`index.js:22`).\n",".claude/rules/dsl-changes.md":"---\npaths:\n  - \"packages/bruno-app/**\"\n  - \"packages/bruno-electron/**\"\n  - \"packages/bruno-cli/**\"\n  - \"packages/bruno-lang/**\"\n  - \"packages/bruno-filestore/**\"\n  - \"packages/bruno-schema/**\"\n  - \"packages/bruno-schema-types/**\"\n  - \"packages/bruno-converters/**\"\n---\n\n# On-Disk DSL & Serialization Changes\n\nBruno persists everything users create — collections, requests, environments, folders, and\nconfig — as plain-text files on the user's disk and in their Git repos. Two on-disk formats\nexist:\n\n- **`.bru`** — the Bru DSL. Read/write goes through the **v2 (ohm-js)** grammar in\n  `packages/bruno-lang/v2/src`, via `packages/bruno-filestore/src/formats/bru`\n  (`bruToJsonV2` / `jsonToBruV2`). Make all `.bru` changes in v2.\n- **`.yml`** — OpenCollection YAML, handled by `packages/bruno-filestore/src/formats/yml`. This is\n  the current `DEFAULT_COLLECTION_FORMAT` (canonical: `packages/bruno-filestore/src/constants.ts`,\n  duplicated for the renderer in `packages/bruno-app/src/utils/common/constants.js`), so new\n  collections are `.yml` unless the user chooses otherwise.\n- Plus `bruno.json` (per-collection config, passed through `stringifyCollection`) and `.env` files\n  (`parseDotEnv` / `dotenvToJson`).\n\n(`packages/bruno-toml` exists but is **orphaned** — imported nowhere outside its own test and on no\nserialization path; a DSL change never needs it. `preferences.json` is app-level electron-store\nstate under `userData`, not part of a collection's on-disk contract, so it is out of scope here.)\n\nThese files outlive the version that wrote them: an older Bruno reads files written by a\nnewer one, teammates on different versions share the same repo, and users hand-commit these\nfiles. **A change to the on-disk shape is a change to a public contract.** Get it wrong and\nyou break parsing, corrupt collections, or silently drop user data — on collections you\ncan't reach to fix.\n\nThe in-memory objects that become these files are often assembled far from the serialization\nlayer — in `bruno-electron` (IPC handlers) or `bruno-app` (Redux) — and handed to\n`bruno-filestore`, which can't tell that a field's shape changed upstream. So a DSL-affecting\nchange can originate in *any* package on the data path, not just the format packages. Treat\nthis rule as in scope for changes anywhere the collection/request/environment/config object\nis built, mutated, or serialized — the serialization packages plus `bruno-app`,\n`bruno-electron`, and `bruno-cli`, where these objects are assembled or read/written. Adding a\npersisted field in app/electron without wiring it through `bruno-filestore` (both formats) and\n`bruno-schema` silently drops or rejects data on save — the exact bug this rule guards against.\n\n## Rules\n\n**Avoid unnecessary DSL changes.** First ask whether the need can be met in memory, in the\nUI, or as a derived value without touching the serialized shape at all — most enhancements\ncan. Only change the on-disk format when the data genuinely must persist.\n\n**Additive and optional only.** A new field must be optional with a safe default, so files\nwithout it still parse and behave. Never rename, remove, repurpose, or retype an existing\nfield, and never change its semantics — old files and old app versions depend on the\ncurrent meaning.\n\n**Round-trip must be lossless.** `parse(stringify(x))` must equal `x`, and `stringify` must\nnever drop fields it doesn't recognize. A file written by a newer version, opened and\nre-saved by an older one, must not lose the newer fields.\n\n**Prove escaping on the written bytes, not in memory.** When a value is embedded in a\ndelimited block (`.bru` multiline `'''…'''`, annotation args), an in-memory `escape`/`unescape`\nround-trip can pass while the on-disk file still corrupts — the parser terminates at the first\nre-introduced delimiter. Test `parse(read(serialize(x)))` on the actual bytes, and fuzz across\nquote/backslash runs. Escape at single-character granularity: escaping a multi-character\ndelimiter by splitting on the whole delimiter (`split(\"'''\")`) leaves fragments that recombine\non runs of length ≥ the delimiter; escape the backslash first, then every single delimiter char.\nRef: `escapeMultilineDescription` in `bruno-lang/v2/src/utils.js`.\n\n**New syntax breaks older parsers — not just older fields.** The additive-and-optional rule\nprotects field-level compat, but adding new `.bru` *syntax* (a new escape form, block delimiter,\nor annotation grammar) makes files written by the new version fail to *parse* in older versions,\nwhich have no handling for it. This is a forward-compat break beyond field loss; it needs an\nexplicit decision and a compatibility path before shipping.\n\n**Descriptions are string-only internally.** Every inline `description` field (headers, params,\nassertions, variables, body entries, ...) is stored as a plain `string`. On the **yml** side,\nparsers accept a bare string *or* a legacy `{ content }` object on read and normalize to the\nstring (`formats/yml/common/{headers,variables,assertions,actions}.ts`, `parseEnvironment.ts`,\n`body.ts`); writers emit a bare string. This read-accepts-object / write-emits-string asymmetry is\nan established convention — not a lossy round-trip — proven by `formats/yml/common/variables.spec.ts`.\nThe `.bru` side has no `{ content }` handling; it stores descriptions as annotation/textblock\nstrings straight from the grammar. New inline description-like fields should follow this\nstring-only shape.\n\n*Exception — `docs` is not string-only.* Collection/folder documentation (`docs`) is written by\nthe **yml** layer as an **object** `{ content, type: 'text/markdown' }`\n(`formats/yml/stringifyCollection.ts`, `stringifyFolder.ts`) and normalized back to a string on\nread; the `.bru` side writes it as a plain textblock. So \"follow the string-only shape\" applies to\ninline `description`, *not* to `docs`-style documentation fields — check which family a new field\nbelongs to before copying either pattern.\n\n**Keep both formats in lockstep.** Any field added or changed must be handled in *both*\n`bru` and `yml` — parse and stringify on each side — or the same collection behaves\ndifferently depending on its format. There is **no dedicated `bru`↔`yml` converter**: a request\nmoves between formats by being parsed in its source format to the shared in-memory object and\nre-stringified in the target format (see `SaveTransientRequest` passing `sourceFormat` +\n`targetFormat` to `renderer:save-transient-request`). So a field handled in only one format is\n**silently dropped** the moment a request is copied/saved from a `.bru` collection into a `.yml`\none (or vice-versa). Pay attention to the *unset* case: a field that defaults or persists one way\nin `.bru` and another in `.yml` (e.g. one path returns `''`, the other `'1'`) is a divergence bug.\nThe default app-data workspace and custom-filesystem workspaces are a second such twin — a value\nthat persists in one but is silently dropped in the other is the same class of bug. Update every\nlayer the field touches: `bruno-schema-types` (types), `bruno-schema` (Yup validation),\n`bruno-filestore` (both formats), `bruno-converters` (import/export), and the grammar in\n`bruno-lang` **v2** if `.bru` syntax itself changes.\n\n**`bruno-schema` (Yup) is not optional — it will reject your field.** The collection/request/\nenvironment Yup schemas are declared `.noUnknown(true).strict()`, and they are validated on the\n**save path** (e.g. `itemSchema.validate` / `environmentSchema.validateSync` in\n`bruno-app`'s `slices/collections/actions.js` and `bruno-electron`'s `store/global-environments.js`).\nA new serialized field that round-trips through filestore but is **not** added to\n`packages/bruno-schema` will throw a validation error on save. `bruno-schema` (runtime Yup) and\n`bruno-schema-types` (TS types) are distinct and both must be updated.\n\n**When a shape must change, migrate on read — never make users edit files.** Add a\nread-time compatibility shim that upgrades the old shape as it is parsed, following the\nexisting patterns: `ensureAuthV3Rc1BackwardsCompatibility` in `formats/yml/parseItem.ts`,\nand the pre-v3 status/statusText swap in `formats/bru/index.ts`. Gate any eventual removal\nbehind a major version bump and leave a dated `TODO(remove after vN)`.\n\n**Design for scale and consistency.** New keys follow the naming and nesting of the existing\n`meta`/section blocks (`meta.name`, `meta.seq`, `meta.type`, `meta.tags`, ...). Prefer\nstructures that extend cleanly — a keyed list over a positional one, an object over a\nwidening union — and match how neighbouring fields are already modelled. Note the on-disk\n`meta {}` block does **not** map 1:1 to the type: in `bruno-schema-types` those keys are flattened\nonto `Item` (`seq`, `name`, `type`, `tags`, `description` directly on the item), and the v2 ohm\ngrammar treats `meta` as an open dictionary — the four names above are the *modeled* ones, not a\nclosed set.\n\n**Name properties like they're permanent — because they are.** A DSL key is written once and\neffectively forever: renaming it breaks every existing file and forces a compat shim. These\nnames are also user-facing — people read and hand-edit `.bru`/`.yml` — and are read by AI\nagents reasoning over collections. Choose clear, descriptive, fully spelled-out names, with\nno cryptic abbreviations or ambiguity, consistent with existing keys, and get it right the\nfirst time. A property name deserves stricter scrutiny than an ordinary variable.\n\n**Test the contract.** Add round-trip tests (parse → stringify → parse) for the new field in\n*both* formats, plus a golden fixture of the *old* format that must still parse unchanged. The\npaired `parseItem.spec.ts` / `stringifyItem.spec.ts` (and `parse*`/`stringify*` for collection,\nfolder, environment) next to each serializer in `bruno-filestore/src/formats/yml` are the pattern;\n`common/variables.spec.ts` and `common/datatype.spec.ts` show field-level cases.\n\n## A new persisted field — the files it usually touches\n\nAdding a single field tends to span several packages (a `.bru`-centric change lands mostly in\n`bruno-lang/v2`):\n\n- `bruno-lang/v2/src` — the `.bru` entry points: `bruToJson.js`, `collectionBruToJson.js`,\n  `envToJson.js`, `jsonToBru.js`, `jsonToCollectionBru.js`, `jsonToEnv.js`, plus `utils.js` for\n  shared helpers.\n- `bruno-filestore/src/formats/yml` — parse **and** stringify for the `.yml` side.\n- `bruno-schema/src/collections/index.js` — the Yup schema (skip this and it fails on save).\n- `bruno-schema-types/src/collection/item.ts` — the TypeScript type.\n- `bruno-converters` — a **separate field list from `bruno-filestore`**, not the same code path:\n  `src/opencollection/items/{http,graphql,grpc,websocket}.ts` maps the field for the OpenCollection schema, might have typescript issues to be consulted with https://github.com/opencollection-dev/opencollection when needed\n  export *and* import, and `src/postman/bruno-to-postman.js` for Postman export. A field wired\n  through filestore but not here round-trips on disk and is silently lost on export.\n- `bruno-app/src/utils/collections/index.js` — `transformRequestToSaveToFilesystem` and siblings\n  whitelist fields before save; a field missing here never reaches the serializer.\n- `bruno-electron/src/ipc/yml-migration.js` — BRU → YML migration (bru parse → yml stringify, plus\n  `bruno.json` → `opencollection.yml` config transforms); verify the field survives migration.\n- the collection utils in `bruno-electron` / `bruno-app` that build the object.\n- Round-trip specs next to the serializers (`formats/yml/parseItem.spec.ts` + `stringifyItem.spec.ts`).\n\nFind a comparable field already in the code and follow how it's wired before adding yours.\n\n## Before changing the DSL — checklist\n\n- [ ] Change is genuinely necessary (can't be solved in memory / UI)\n- [ ] New field is optional with a safe default; nothing renamed, removed, or retyped\n- [ ] Handled in both `bru` and `yml`, parse **and** stringify\n- [ ] Types (`bruno-schema-types`), Yup schema (`bruno-schema`), and converters updated —\n      \"converters\" means `bruno-converters/src/opencollection/items/*` in **both** directions,\n      which is a different field list from `bruno-filestore`\n- [ ] Field survives the app save transform and BRU → YML migration (see `feature-parity.md` §2)\n- [ ] Old files still parse — read-time compat shim added if the shape changed\n- [ ] No new `.bru` syntax that older parsers can't read (or a forward-compat path decided)\n- [ ] Any new escaping proven via on-disk reparse + fuzz, escaping single chars not the delimiter\n- [ ] `bru`/`yml` (and default vs custom workspace) behave identically, including the unset case\n- [ ] Round-trip + old-format fixture tests added\n- [ ] Naming/structure consistent with existing blocks and scalable\n",".claude/rules/electron-ipc.md":"---\npaths:\n  - \"packages/bruno-electron/**/*\"\n---\n\n# Electron Main Process Guide\n\nThe main process (`packages/bruno-electron/src/index.js`) owns the filesystem, network, child\nprocesses, and all electron-store data. The renderer reaches it **only** over IPC through the\npreload bridge — there is no direct Node access in the renderer despite `nodeIntegration: true`\n(see Security below). Before adding or changing any IPC, read this file plus the target\n`src/ipc/<domain>.js` handler.\n\n## Preload bridge & renderer access\n\n`src/preload.js` is the *only* surface the renderer sees. It exposes, via\n`contextBridge.exposeInMainWorld('ipcRenderer', …)`, exactly:\n\n- `invoke(channel, ...args)` → `ipcRenderer.invoke` (renderer→main request/response)\n- `send(channel, ...args)` → `ipcRenderer.send` (fire-and-forget)\n- `on(channel, handler)` → subscribe; **returns an unsubscribe function** and **strips the\n  Electron `event`/`sender` arg** — renderer handlers receive `(...args)`, not `(event, ...args)`\n- `getFilePath(file)` (via `webUtils.getPathForFile`) and `openExternal(url)` (via `shell.openExternal`)\n\nAlso exposed: `window.isPlaywright` (`process.env.PLAYWRIGHT === 'true'`).\n\nRenderer code accesses this as `window.ipcRenderer` (e.g. `const { ipcRenderer } = window;` in\n`bruno-app/src/providers/App/useIpcEvents.js`). **There is no channel allowlist** — any channel\nstring is forwarded. Do not rely on the preload to gate channels; validate inputs inside the\nhandler.\n\n## Channel naming convention\n\nFollow the prefix that matches the direction — the vast majority of handlers already do:\n\n- **`renderer:*`** — renderer→main via `ipcMain.handle` (the default for new work; ~255 of ~290\n  handlers). e.g. `renderer:ready`, `renderer:save-request`, `renderer:create-workspace`,\n  `renderer:browse-directory`, `renderer:get-global-environments`.\n- **`main:*`** — main→renderer push via `webContents.send`. e.g. `main:load-preferences`,\n  `main:collection-opened`, `main:workspace-opened`, `main:collection-tree-updated`,\n  `main:display-error`, `main:app-loaded`. A few `main:*` names are *internal* main-process\n  events routed through `ipcMain.emit`/`ipcMain.on` (`main:renderer-ready`,\n  `main:workspaces-ready`) — see the startup sequence.\n- **Domain-namespaced invokes** for stateful sub-protocols: `grpc:*`, `renderer:ws:*` (WebSocket),\n  `terminal:*`, `menu:*` (menu→main), `internal:*` (main-to-main only).\n- **Legacy unprefixed network channels** — `send-http-request`, `cancel-http-request`,\n  `fetch-gql-schema`, `clear-oauth2-cache`, `connections-changed`. These predate the convention;\n  prefer a `renderer:*` name for new request-side channels rather than adding to this unprefixed set.\n\n## Handler registration\n\nAll handlers are registered inside `app.on('ready')` in `index.js` (the `// register all ipc\nhandlers` block) by calling per-domain `register*Ipc(mainWindow, …)` functions, each `require`d\nat the top of `index.js`. `registerPreferencesIpc` (which kicks off onboarding + the\n`renderer:ready` handler) runs *before* `registerWorkspaceIpc` (which owns `main:renderer-ready`);\nthis is safe because `renderer:ready` only fires after the renderer mounts, by which point every\nhandler is registered. **A new IPC domain must add its own `register*Ipc` call here** — a handler\nfile that is never `require`d + called registers nothing.\n\n## Where new handler code goes — keep it small and testable\n\n**Don't let a handler file keep growing to where its size hurts readability and maintainability.**\nWhen a group of handlers forms a separable feature, give it its own `ipc/<domain>.js` with its own\n`register*Ipc` (wired into `index.js` as above) rather than appending to an existing large file —\nseparate it whenever it makes sense. Splitting by domain is already the norm here (`apiSpec.js`,\n`workspace.js`, `mount.js`, …).\n\n**Structure a handler so its callback is unit-testable.** Write the logic as a named, exported\nfunction and let the thin `ipcMain.handle(channel, fn)` line just call it — not an inline anonymous\narrow, which can only be exercised through Electron. The exported callback can then be tested\nwithout spinning up the main process: see `ipc/swagger-fetch.js` (`proxySwaggerFetch`) and the\nspecs under `ipc/network/` and `ipc/ai/`.\n\n## Error-handling convention\n\nTwo patterns by handler type — match the one that fits:\n\n- **CRUD / lifecycle handlers throw or reject.** Validation failures use bare\n  `throw new Error(...)`; caught errors are re-surfaced with `return Promise.reject(error)` or\n  `throw error`. The renderer `invoke` promise rejects and is shown via a `main:display-error`\n  toast (`useIpcEvents.js`). A few read-only probes intentionally swallow and return a fallback\n  (e.g. `exists-sync` → `false`, `get-collection-workspaces` → `[]`).\n- **Request-execution handlers embed the error in the result.** `send-http-request`\n  (`ipc/network/index.js`) does **not** reject on a failed request/script/test — it returns a\n  result object carrying an `error` field so the renderer can render the failure as data. New\n  network handlers follow this embed-in-result shape; new CRUD handlers throw/reject.\n\n## Security constraints\n\n`BrowserWindow` `webPreferences` (in `index.js`): `nodeIntegration: true`,\n`contextIsolation: true`, `preload: preload.js`, `webviewTag: true`. **This `nodeIntegration:\ntrue` + `contextIsolation: true` combination is intentional — do not \"fix\" it.** No `sandbox`\nkey (defaults off); `webSecurity` defaults on. A CSP is set via `electron-util`'s\n`setContentSecurityPolicy` in `index.js` (`default-src 'self'`; `connect-src` adds PostHog;\n`script-src 'self' data:`; `img-src` allows http/https/blob/data). `form-action 'none'` is\ndeliberately commented out to keep OAuth2 working. External navigation is hardened via\n`will-redirect` + `setWindowOpenHandler`, which route http/https to `shell.openExternal` and deny\neverything else — preserve this when touching window/navigation code.\n\n## IPC startup sequence\n\n```\nrenderer mounts\n  -> useEffect: ipcRenderer.invoke('renderer:ready')    [useIpcEvents.js]\n     -> main handles 'renderer:ready'                    [ipc/preferences.js]\n          await onboardingPromise\n          send main:load-preferences\n          send main:load-global-environments (+ main:git-version, reopen last apispecs, ...)\n          ipcMain.emit('main:renderer-ready')\n            -> workspace.js listener:                     [ipc/workspace.js]\n                 await ensureDefaultWorkspaceExists()\n                 send main:workspace-opened (per workspace)\n                 send + ipcMain.emit('main:workspaces-ready')\n                   -> onboarding.js sends main:collection-opened  [app/onboarding.js]\n\ndid-finish-load (independent, from Chromium):                     [index.js]\n  -> wraps webContents.send with a safeStringify/safeParse JSON roundtrip\n  -> send main:app-loaded\n     -> renderer sink sets data-app-state=\"loaded\"        [pages/Bruno/index.js, NOT useIpcEvents.js]\n```\n\n## IPC race-condition pattern\n\nWhen `ipcMain.emit` triggers multiple `ipcMain.on` listeners:\n- Sync listeners complete fully before the next starts.\n- Async listeners yield at the first `await`, letting the next listener start.\n- `webContents.send` from earlier listeners can arrive at the renderer before messages from\n  later async continuations.\n\nFix: chained events (`main:renderer-ready` → `main:workspaces-ready` → deferred\n`main:collection-opened`) enforce ordering. Reuse this pattern rather than adding `setTimeout`s.\n\n## Persistent stores (`src/store/`, electron-store)\n\nStores are electron-store JSON files under `app.getPath('userData')`, one module per file —\n**read the directory for the current set** (it is large: preferences, cookies, oauth2,\ncollection-security, secrets, workspace/global environments, last-opened-*, window-state, …).\nGotcha: multiple modules can share the *same* JSON file. `lastOpenedCollections` lives in\n`preferences.json` but is owned by `store/last-opened-collections.js` (a distinct\n`new Store({ name: 'preferences' })` module), **not** by `store/preferences.js`. Don't assume a\nstore key belongs to the module named after the file.\n\n## File watchers (`src/app/`)\n\nWatchers live in `src/app/` — **read the directory for the current set** (`collection-watcher.js`,\n`workspace-watcher.js`, `apiSpecsWatcher.js`, `dotenv-watcher.js`). Cleanup is\n**not** uniform:\n- `closeAllWatchers()` in `index.js` tears down exactly three —\n  `collectionWatcher`, `workspaceWatcher`, `apiSpecWatcher` — each of which exposes\n  its own `closeAllWatchers()`.\n- `dotenv-watcher.js` is a shared singleton exposing `closeAll()` and is torn down transitively by\n  the workspace watcher (and driven by the collection watcher), **not** from `index.js`.\n\nWhen adding a watcher, decide which lifecycle it belongs to and register its cleanup in the\nmatching place — a top-level watcher goes into `closeAllWatchers()` in `index.js`; a\nper-workspace/per-collection one hangs off the owning watcher.\n\n## Onboarding flow\n\nEntry: `src/app/onboarding.js` (`onboardUser`), invoked synchronously when `registerPreferencesIpc`\nruns.\n\n1. Checks `preferencesUtil.hasLaunchedBefore()`.\n2. New users: imports the sample collection from `resources/data/sample-collection.json`.\n3. Defers `main:collection-opened` (via a pending-collection var) until `main:workspaces-ready`.\n4. `DISABLE_SAMPLE_COLLECTION_IMPORT === 'true'` skips the import (set in Playwright).\n\n## Key environment variables\n\n- `ELECTRON_USER_DATA_PATH` — custom userData path, **dev mode only** (`index.js:22`, guarded by `isDev`).\n- `DISABLE_SAMPLE_COLLECTION_IMPORT` — skip sample collection on first launch.\n- `PLAYWRIGHT` — signals the app is running under Playwright (read in `preload.js`, stores, etc.).\n- `DISABLE_SINGLE_INSTANCE` — allow multiple app instances.\n\n(All four are set to `'true'` for e2e in `playwright/index.ts`.)\n\n## Before adding or changing an IPC handler — checklist\n\n- [ ] Handler added in `src/ipc/<domain>.js` as `ipcMain.handle('renderer:<verb>', …)` (or the\n      matching domain prefix); no new unprefixed channel.\n- [ ] New handler lives in a focused `ipc/<domain>.js` — a separable feature gets its own file\n      rather than bloating an existing one; its callback is a named exported function\n      (unit-testable), not an inline arrow.\n- [ ] If it's a new domain, its `register*Ipc(mainWindow, …)` is called in `index.js`'s ready block.\n- [ ] Inputs validated inside the handler (the preload has no allowlist).\n- [ ] Error path matches the handler type — CRUD throws/rejects; request-execution embeds `error`\n      in the result.\n- [ ] For a `main:*` push channel, the renderer adds a listener **and** an unsubscribe in\n      `useIpcEvents.js`'s cleanup.\n- [ ] Cross-process values survive the `did-finish-load` JSON roundtrip (no functions,\n      class instances, or circular refs in payloads).\n",".claude/rules/feature-parity.md":"---\npaths:\n  - \"packages/**/*\"\n---\n\n# Co-Related Features & Parity Surfaces\n\nBruno implements the same behaviour more than once — app vs CLI, `.bru` vs `.yml`, filestore vs\nconverters, implementation vs QuickJS shim. **A change to one side of a pair is not a finished\nchange**: the other side silently keeps the old behaviour and ships. The symptom is never a broken\nbuild — it's a feature that works in the app but not in `bru run`, a field that survives save but\nvanishes on export or migration, a `bru.*` method that is `undefined` in the default sandbox.\n\nThis rule maps *which* surfaces pair up. `.claude/rules/dsl-changes.md` owns the on-disk contract\nin depth.\n\n## When you work on a change\n\n1. **Find the twin before writing the fix.** For every file you touch in the tables below, open\n   its counterpart.\n2. **Update the twin in the same change** when it's within what was asked.\n3. **Otherwise, say so explicitly.** If the twin is out of scope, doesn't apply, or you're unsure,\n   list it in your reply to the user (and in the PR description) with the path and why it was left\n   — never leave a one-sided change unmentioned.\n4. **Prefer unifying over copying.** If the logic has no Electron- or CLI-only dependency, move it\n   into `@usebruno/requests` / `@usebruno/common` (respecting `.claude/rules/architecture.md`)\n   instead of patching twice. Precedent: `utils/cookies.js` is one line in each runtime —\n   `module.exports = require('@usebruno/requests').cookies;`. Where both copies must stay, note the\n   pairing in a comment at both sites.\n\n## 1. App ↔ CLI — highest priority\n\n`bruno-cli` is how collections run in CI. Several pipeline modules are **copied, not shared**, and\na bug fixed in the app is still present in `bru run` until its twin is fixed. Trace app-reported\nbugs to the shared logic, not the reporting surface.\n\n| Concern | `bruno-electron/src/…` | `bruno-cli/src/…` |\n|---|---|---|\n| Request execution — pre/post scripts, vars, asserts, tests, runner flow (`setNextRequest`, `skipRequest`) | `ipc/network/index.js` | `runner/run-single-request.js`, `commands/run.js` |\n| Request assembly | `ipc/network/prepare-request.js` | `runner/prepare-request.js` |\n| HTTP client, TLS, certs | `ipc/network/axios-instance.js` | `utils/axios-instance.js` |\n| Interpolation | `ipc/network/interpolate-{vars,string}.js` | `runner/interpolate-{vars,string}.js` |\n| Collection tree, `merge{Headers,Scripts,Vars,Auth}` | `utils/collection.js` | `utils/collection.js` |\n| Collection loading, `bruno.json` / `opencollection.yml` config | `utils/collection-reader.js`, `app/collection-watcher.js` | `utils/collection.js` (`getCollectionConfig`) |\n| Environments & persisted vars | `utils/environments.js` | `utils/environment.js`, `utils/persist-variables.js` |\n| Proxy · OAuth 2.0 · form-data · filesystem | `utils/{proxy-util,oauth2,form-data,filesystem}.js` | `utils/{proxy-util,oauth2,form-data,filesystem}.js` |\n| AWS SigV4 | `ipc/network/awsv4auth-helper.js` | `runner/awsv4auth-helper.js` |\n\n- A new app runner option (tags, delay, bail) needs a `bru run` flag, or a stated reason.\n- Import: the app imports Postman, Insomnia, OpenAPI, WSDL and OpenCollection; `bru import`\n  (`commands/import.js`) only OpenAPI and WSDL. Changes to a shared converter affect both.\n- **Known-divergent by design.** The CLI has no preferences store (so no `resolveInheritedSettings`),\n  no Electron window, no system-browser OAuth, and no `@usebruno/schema` validation. Its runner\n  supports only `http-request` and `graphql-request`: it has no gRPC or WebSocket transport, and\n  every request item goes through the HTTP pipeline in `runner/run-single-request.js`.\n\n## 2. Persisted fields — every layer that carries a field list\n\nA field is added in one place and read in many. Each of these keeps its **own** list, aligned by\nhand:\n\n| Layer | Where | Missed → |\n|---|---|---|\n| Types | `bruno-schema-types/src/collection/item.ts` | type drift |\n| Yup schema | `bruno-schema/src/collections/index.js` — `.noUnknown(true).strict()` | **throws on save** |\n| App save transform | `bruno-app/src/utils/collections/index.js` — `transformRequestToSaveToFilesystem`, `transform{Collection,Folder}RootToSave`, `transformCollectionToSaveToExportAsFile` | dropped before reaching disk / Bruno JSON export |\n| On-disk formats | `bruno-filestore/src/formats/{bru,yml}` (+ `bruno-lang`) — parse **and** stringify | lost in one format |\n| BRU → YML migration | `bruno-electron/src/ipc/yml-migration.js` (bru parse → yml stringify) | lost when a user migrates |\n| Collection config | `bruno.json` ↔ `opencollection.yml` — `utils/transformBrunoConfig.js`, `stringifyCollection`, migration's config transform (e.g. `transformProxyConfig`), CLI `getCollectionConfig` | config lost or misread by CLI |\n| OpenCollection import/export | `bruno-converters/src/opencollection/items/{http,graphql,grpc,websocket}.ts`, `folder.ts`, `environment.ts` — **both** directions; app entry `bruno-app/src/utils/exporters/opencollection.js` | lost on export or import |\n| Other converters | `postman/{postman-to-bruno,bruno-to-postman}.js`, `insomnia/`, `openapi/`, `wsdl/`; app exporters `postman-collection.js`, `openapi-spec.js`, `bruno-environment.js` | lost on import/export where the target can represent it |\n\nThe migration also rewrites content, not just shape: `bru.runRequest(\"x.bru\")` paths are stripped\nof `.bru`, and tab snapshots are remapped. A new script API that takes a file path, a new file\ntype, or a new `bruno.json` key needs a matching migration step.\n\n## 3. Scripting API ↔ QuickJS shim\n\n`bruno-js/src/{bru,bruno-request,bruno-response}.js` implement the API. The **QuickJS sandbox — the\ndefault \"safe\" mode — bridges each method explicitly** in `bruno-js/src/sandbox/quickjs/shims/`\n(`bru.js`, `bruno-request.js`, `bruno-response.js`, `bruno-grpc.js`, …). Implementation only →\nworks in developer mode, `undefined` for most users.\n\n## 4. Scope levels — collection ↔ folder ↔ request\n\nAuth, headers, vars, scripts and tests exist at three levels plus `inherit`. Each level has its own\nUI (`CollectionSettings/`, `FolderSettings/`, `RequestPane/`), and resolution lives in the merge\nhelpers in **each** runtime's `utils/collection.js`. A feature at one level needs a stated decision\nfor the other two and for `inherit`.\n\n## 5. Protocols — HTTP ↔ GraphQL ↔ gRPC ↔ WebSocket\n\n`REQUEST_TYPES` (`bruno-app/src/utils/common/constants.js`, `bruno-electron/src/utils/constants.js`)\nlists all four. Each has its own `parse*`/`stringify*` in `bruno-filestore/src/formats/yml/items/`\nand its own converter in `bruno-converters/src/opencollection/items/`. \"HTTP only\" is a legitimate\nscope — leaving it unstated is how a gap becomes a bug report. Widening CLI protocol support is a\ndeliberate decision, not an incidental one.\n\n## 6. Request shaping ↔ code generation\n\n`GenerateCodeItem/utils/snippet-generator.js` consumes headers, body, `settings` and resolved auth.\nA new auth mode or a setting that changes the wire request should change the generated snippet.\n\n## Parity checklist\n\n- [ ] Every twin in §1 opened; updated, or listed to the user with a reason\n- [ ] App-reported bug fixes verified on the CLI path\n- [ ] Shared logic moved to a leaf package where the DAG allows, instead of patched twice\n- [ ] Persisted field carried through every layer in §2 — including migration and converters (both directions)\n- [ ] New/changed `bru.*`, `req.*`, `res.*` API bridged in the QuickJS shim\n- [ ] Collection / folder / request levels and `inherit` each decided\n- [ ] gRPC and WebSocket considered; any \"HTTP only\" scope stated\n- [ ] Code generation updated if the wire request changed\n- [ ] Tests on **both** sides of every pair touched\n\nHuman-facing version of this map (for QA): Confluence *Related Feature Map — Change Impact &\nParity Checklist*, https://usebruno.atlassian.net/wiki/spaces/Bruno/pages/1376944136. Keep the two\nin step: add new pairings to both, and remove unified ones from both.\n",".claude/rules/redux-store.md":"---\npaths:\n  - \"packages/bruno-app/**/*\"\n---\n\n# React / Redux Architecture\n\n## Store configuration\n\n`configureStore` (Redux Toolkit **v1.8**, not v2) is set up in\n`src/providers/ReduxStore/index.js`. That file's `reducer` map is the **authoritative slice list**\n(~11 keys); read it rather than trusting any list here. Middleware is\n`getDefaultMiddleware().concat([...])` with **no options passed**, so RTK's defaults —\n`thunk`, plus `serializableCheck` and `immutableCheck` in dev — are all active.\n\n**Consequences to respect:**\n- **State must stay serializable.** Don't put functions, class instances, Promises, or DOM nodes\n  in the store — dev `serializableCheck` will flag them. Do **not** disable `serializableCheck` in\n  the real store to work around it (the only place it's disabled is a middleware unit test).\n- **Reducers mutate the Immer draft directly.** `createSlice` reducers write\n  `state.collections.push(...)`, `item.name = ...`, `.splice(...)` — do **not** spread-clone or\n  return new objects unless replacing wholesale. Match the surrounding reducer.\n\n## Slice organization\n\nSlices live in `src/providers/ReduxStore/slices/`, one domain each; `collections/` is by far the\nlargest (collection tree, requests, environments, runner, drafts — ~200 reducer action creators in\n`collections/index.js`, plus ~150 thunks in `collections/actions.js`, and more spread in from\n`collections/exampleReducers.js`). Read the specific slice for its real state and actions.\n\nTwo gotchas:\n- **Reducer key ≠ folder/file name.** e.g. `globalEnvironments` comes from\n  `slices/global-environments.js`; `openapiSync` from `slices/openapi-sync.js`.\n- **Action-type prefix follows the slice's `name:`, not the reducer key.** e.g.\n  `global-environments/…`, `collections/…`, `app/…`. Grep the action type, not the key, when\n  tracing dispatches.\n\n## Actions / thunks convention\n\nAsync logic is written as plain hand-written thunks — a function that returns\n`(dispatch, getState) => …`. `createAsyncThunk` isn't used anywhere today:\n\n```javascript\nexport const saveRequest = (itemUid, collectionUid) => (dispatch, getState) => { ... };\n// or async\nexport const openCollectionEvent = (uid, pathname, ...) => async (dispatch, getState) => { ... };\n```\n\nThunks live in the slice's `actions.js` (`slices/collections/actions.js` is the big one:\n`sendRequest`, `saveRequest`, `importCollection`, `pasteItem`, `deleteItem`, `openCollectionEvent`\n— the last handles the `main:collection-opened` IPC). Reducers (in the slice's `index.js`) stay\nsynchronous. Follow the plain-thunk shape by default so new code matches the rest — but this is a\nconvention, not a blocker: if a feature genuinely benefits from `createAsyncThunk` (e.g. its\nbuilt-in pending/fulfilled/rejected states), it's fine to reach for it deliberately.\n\n## Side-effect boundaries\n\n- **Reducers are pure** — no IPC, no network, no `Date.now`/random, no async. Immer draft\n  mutations only.\n- **Side effects live in thunks and middleware.** IPC is invoked from thunks via\n  `window.ipcRenderer` (e.g. `saveRequest` → `ipcRenderer.invoke('renderer:save-request', …)`); the\n  `collections/actions.js` file alone has ~200 `ipcRenderer` calls.\n- **The file-IO → open-tab pattern** is split across a thunk and the `tasks` middleware: a thunk\n  queues an `OPEN_REQUEST`/`OPEN_EXAMPLE` task (`insertTaskIntoQueue`) and fires the IPC write; the\n  `tasks` middleware watches for the resulting `main:*` file event, then dispatches `addTab` +\n  `removeTaskFromQueue`. Reuse this queue rather than opening the tab optimistically.\n\n## Custom middleware (`middlewares/`)\n\nFive middlewares, registered in `index.js` (read the directory + registration array for the live\nset):\n- **`tasks`** — listener middleware; opens a request/example tab after the file event for a queued\n  task arrives (see file-IO pattern above).\n- **`draft`** — on the first edit action, promotes a *preview* tab to permanent\n  (`handleMakeTabParmanent`). It does **not** write draft state — the reducers do.\n- **`autosave`** — debounced by `preferences.autoSave.interval`, gated on\n  `app.preferences.autoSave.enabled`; dispatches the `save*` thunks.\n- **`snapshot`** — debounced (~1s), gated on `app.snapshotReady`; persists a curated state subset\n  to disk (see Persistence).\n- **`debug`** (dev only) — logs every action; added only when `import.meta.env.MODE ===\n  'development'`.\n\n## Persistence / serialization constraints\n\nThe store isn't persisted wholesale, and there's no `redux-persist`. Instead the `snapshot`\nmiddleware serializes a **curated subset** — workspaces, collection metadata, per-collection tabs,\nselected environments, and devtools console open-state (`extras.devTools`, from the `logs` slice;\nsee `middlewares/snapshot/serializeSnapshot.js`) — and writes it to disk via IPC. Request/response\nbodies are deliberately not persisted. To persist a new piece of state, extend `serializeSnapshot`\nand its save-trigger list (`utils/snapshot`) — the snapshot is the established mechanism, so prefer\nit over adding `redux-persist` or `localStorage`.\n\n## Selectors\n\nInline `useSelector` in components is the norm (hundreds of usages); memoized `createSelector` is\nused in just one place (`src/selectors/tab.js`). Default to inline\n`useSelector`, and reach for `createSelector` when a derivation is genuinely expensive or reused.\n\n## What belongs in Redux (and what doesn't)\n\n- **In Redux:** draft / unsaved state (`item.draft`, `collection.draft`, `environmentsDraft`,\n  `globalEnvironmentDraft`), and cross-component or persisted UI state (`sidebarCollapsed`,\n  `leftSidebarWidth`, `taskQueue`, `item.collapsed`, active tab).\n- **Local `useState`:** ephemeral, component-scoped UI — search text, inline-create toggles, hover,\n  transient form state. Don't lift these into a slice.\n\n## Sidebar collection visibility\n\nCollections render in the sidebar only when they belong to the **active workspace**.\n`Sidebar/Collections/index.js` builds the list in a `useMemo` that walks\n`activeWorkspace.collections`, matches loaded collections through a `Map` keyed on\n`normalizePath(pathname)`, **excludes scratch collections** (`isScratchCollection`), adds \"ghost\"\ngit-remote rows (`GitRemoteCollectionRow`) for non-default workspaces, and sorts alphabetically.\nRead the current `useMemo` before changing this. Collapsed folders do **not** render their children\nin the DOM (conditional render on `item.collapsed`).\n\n## Sidebar DOM structure (for Playwright)\n\n`.collection-item-name` is the class on the **row-wrapper** div for every item (folders, requests,\nJS files) in `Sidebar/Collections/Collection/CollectionItem/index.js` — the wrapper also contains\nthe name span and `.menu-icon`. Use it for Playwright locators.\n\n## Providers (`src/providers/`)\n\nRead the directory for the current set: `App/` (IPC listeners in `useIpcEvents.js`, app init),\n`ReduxStore/`, `Theme/`, `Hotkeys/`, `Toaster/`, `PromptVariables/`.\n\n## Checks after changing a slice / middleware\n\nSlices and middleware have Jest coverage under `src/providers/ReduxStore/` (e.g.\n`slices/collections/*.spec.js`, `slices/global-environments.spec.js`,\n`middlewares/snapshot/serializeSnapshot.spec.js`). After a change, run the relevant spec:\n\n```bash\nnpm test --workspace=packages/bruno-app -- <path-or-pattern>\n```\n\nIf you touch the reducer map, thunks that fire IPC, or the snapshot subset, also sanity-check that\nstate stays serializable (no dev `serializableCheck` warnings) and that persisted shape still\nround-trips through `serializeSnapshot`.\n",".claude/rules/sqlite.md":"---\npaths:\n  - \"packages/bruno-sqlite/**/*\"\n  - \"packages/bruno-electron/src/ipc/sqlite.js\"\n  - \"packages/bruno-electron/src/services/sqlite/**/*\"\n---\n\n# SQLite SDK (`@usebruno/sqlite`)\n\nBruno's local database layer. You author **migrations** (`migrations/*.ts`) and **statements**\n(`statements/*.sql`); a codegen step compiles them into a typed data layer for the Electron main\nprocess, which owns the database and executes statements. The package has no renderer entry point:\nthe renderer reaches a statement only through an IPC handler written by hand for that statement in\n`bruno-electron`.\n\n**The renderer never sends SQL, and never picks the statement.** Each IPC channel is bound to exactly\none statement in the main process; the renderer sends only that statement's params. Do not add an\nIPC handler, statement, or option that accepts SQL text or a statement name from the renderer.\n\n## When to use it — and when not to\n\nUse it for state that is **derived, bulky, or secondary**: data Bruno can rebuild or lose without\ncorrupting anything the user authored, and that would bloat Redux or the IPC payloads if held in\nmemory.\n\nIt is **not** the right home for:\n\n- **Anything the user authored.** Collections, requests, environments, folders and `bruno.json` live\n  on disk as `.bru`/`.yml` — see `.claude/rules/dsl-changes.md`. Never mirror or migrate a\n  serialized field into SQLite.\n- **App preferences and small keyed state** — those are electron-store JSON files under\n  `src/store/` (see `.claude/rules/electron-ipc.md`).\n- **UI state** — Redux, or local `useState` (see `.claude/rules/redux-store.md`).\n\nThe database file is `bruno.db` under `app.getPath('userData')`, and the file store spills large\npayloads to `sqlite-files/` beside it. **Treat both as disposable**: the open path below deletes\nand rebuilds the database rather than failing (startup `collect()` then sweeps the spilled files no\nrow names), so never let either hold the only copy of anything.\n\n## Package layout\n\n```\npackages/bruno-sqlite/\n  migrations/<seq>_<name>.ts   authored — exports up()/down() returning SQL\n  statements/*.sql             authored — sqlc-style annotated statements\n  scripts/                     codegen (generate-artifacts), new-migration, verify-migrations,\n                               generate-schema\n  SCHEMA.md                    GENERATED, committed — ER diagram + table schemas\n  src/node/                    db.ts (migrations), statements.ts (prepare/execute)\n  src/shared/                  shared types\n  src/generated/               GENERATED, gitignored — never edit or commit\n  tests/node\n```\n\n**`@usebruno/sqlite` (= `/node`) is main-process only** (it imports `node:sqlite`, `node:fs`,\n`node:crypto`); never import it from the renderer. The package has zero internal `@usebruno/*`\ndependencies and must stay that way.\n\n`src/generated/` is produced from the authored migration and `.sql` files by `npm run generate`\n(which `prebuild`/`pretest` run for you). Those authored files are the only source of truth.\n\n## Adding a migration\n\n```bash\nnpm run migration:new --workspace=packages/bruno-sqlite    # prompts for a name\n```\n\nThis scaffolds `migrations/<7-digit sequence>_<name>.ts` exporting `up` and `down`, each returning\nthe SQL to apply or revert:\n\n```ts\nexport const up = (): string => {\n  return 'CREATE TABLE users (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, email TEXT);';\n}\nexport const down = (): string => {\n  return 'DROP TABLE users;';\n}\n```\n\nMigrations run at open time, in sequence order, each in its own transaction, and are recorded in a\n`_migrations` table with a sha256 of both `up` and `down`.\n\n- **A merged migration is immutable.** Editing one changes its hash; the next open throws\n  `does not match the migration already applied`, and `createDatabase` responds by **deleting the\n  user's database and rebuilding it**. Add a new migration instead — always.\n- **`down` must genuinely revert `up`.** It is not decoration: when the tail migration disappears\n  from the code (a user downgrading Bruno, or you switching branches), the recorded `down` is\n  executed automatically against the existing database. An empty or wrong `down` corrupts that path.\n- Only the **tail** rolls back (recorded sequences above the highest local one). Deleting a\n  migration from the middle silently leaves its schema in place — don't.\n- Design schema changes to tolerate an older Bruno opening the same file afterwards: prefer new\n  tables and nullable columns over reshaping an existing one.\n\nVerify against a real database rather than an empty one — this replays every migration on a\nthrowaway `VACUUM INTO` copy and then prepares every statement against the result:\n\n```bash\ncp .env.example .env    # set DB_PATH to an absolute path to a real bruno.db\nnpm run migration:verify --workspace=packages/bruno-sqlite\n```\n\n`SCHEMA.md` documents the resulting schema: a Mermaid ER diagram plus each table's columns, keys and\nindexes, produced by applying every migration to an empty in-memory database. The pre-commit hook\nregenerates and stages it whenever a file under `migrations/` is staged; run\n`npm run schema --workspace=packages/bruno-sqlite` to regenerate it by hand. Never edit it directly.\n\n## Adding statements\n\nStatements live in `statements/*.sql`, several per file, each introduced by a sqlc-style\nannotation. Params are **named** and always passed as an object — there is no positional binding:\n\n```sql\n-- name: list_users :many\nSELECT id, name, email FROM users ORDER BY name;\n\n-- name: get_user :one\nSELECT id, name, email FROM users WHERE id = @id;\n\n-- name: create_user :exec\nINSERT INTO users (name, email) VALUES (@name, @email);\n```\n\n| annotation | runs as | returns |\n|---|---|---|\n| `:one` | single row | row or `undefined` |\n| `:many` | rows | array |\n| `:exec` | write | `{ changes, lastInsertRowid }` |\n\n- Statement names are **globally unique across all `.sql` files** (a duplicate fails codegen).\n  Renaming a statement, or moving it to another file, means renaming its IPC channel and every\n  renderer call site too.\n- Use `@param` for consistency with the existing statements, even though `node:sqlite` also accepts\n  `:name` and `$name`.\n\n## Main process\n\n`packages/bruno-electron/src/services/sqlite/index.js` owns the single database instance:\n`openDatabase()` calls `createDatabase` on `bruno.db` and starts a background `files.collect()`; the\nservice exposes `getStatements()`, `getFiles()` (the SDK's file store — see the package README),\n`transaction(callback)`, `reclaimDiskSpace()` and `shutdown()`. It never hands out the `DB` itself,\nand it knows nothing about IPC. `index.js`'s ready block opens it (`sqliteService.openDatabase()`),\nand `before-quit` reclaims disk space and shuts it down.\n\n```js\nconst { getStatements, transaction } = require('../../services/sqlite');\n\ntry {\n  transaction(() => {\n    getStatements().execute('create_user', { name, email });\n  });\n} catch (err) {\n  // degrade: the database may be unavailable\n}\n```\n\n`getStatements()` never returns `null`. Before `openDatabase()`, after shutdown, and when\n`createDatabase` returns `{ db: undefined, statements: undefined }` (even the in-memory fallback\nfailed), it returns a stub whose `execute` logs the skipped statement and throws; `getFiles()`\nreturns a matching stub whose methods log and throw (or reject, for the async ones), and\n`transaction` throws the same way. So there is one failure mode, and every caller must handle it:\n\n- **`execute` and `transaction` throw** when the database is unavailable, for an unknown statement,\n  for one that couldn't be prepared against this schema (prepare failures are logged at construction\n  and only that statement is discarded — the rest of the database stays usable), and for any SQL\n  error. Wrap calls in try/catch and stay useful without the database — degrade, don't crash.\n\n`createDatabase` degrades rather than failing: file → (migration error) delete the `bruno.db*` files\nand rebuild → in-memory → `undefined`. A real backup is still a `TODO` in `src/node/index.ts`; it\ndeletes today.\n\n**`node:sqlite` is synchronous — every `execute` blocks the main process.** Keep statements indexed,\nbounded, and off hot paths; no full-table scans, no unbounded result sets, no per-event writes in a\ntight loop.\n\n## Exposing a statement to the renderer\n\nOnly statements the renderer actually needs are exposed, each with its own hand-written\n`ipcMain.handle` in `packages/bruno-electron/src/ipc/sqlite.js`. `registerSqliteIpc()` is called in\n`index.js` right after `openDatabase()`; each handler calls the service's `getStatements()` per\ninvocation, so a call after shutdown hits the stub, not a closed database.\n\nThe channel name follows a fixed convention: **`datastore:<statement_file>:<statement_name>`**, where\n`<statement_file>` is the `.sql` file's name without the extension, written exactly as the file is\nnamed. Channel names are written by hand, not generated.\n\n```js\nipcMain.handle('datastore:runner_responses:get_runner_response', (_event, params) => {\n  const request_uid = requireUid(params?.request_uid, 'request_uid');\n  return getStatements().execute('get_runner_response', { request_uid });\n});\n```\n\n- The handler validates and picks the params it forwards — never pass the renderer's object\n  straight through to `execute`.\n- The file store has no channels of its own. File ids are sequential and unscoped, so a channel\n  that takes one would expose every stored blob; the renderer reaches files only through a\n  domain handler that resolves them from a scoped key (e.g. `get_runner_response` by\n  `request_uid`). File writes stay main-process only — the renderer never chooses what gets\n  stored.\n- In the renderer, wrap each channel in a named hook or helper under `src/hooks/useX/index.js`\n  (or the relevant util) and call `window.ipcRenderer.invoke(channel, params)` there; no component\n  invokes a `datastore:*` channel directly.\n\n## Commands\n\n```bash\nnpm run build:bruno-sqlite                          # generate + rollup (from the repo root)\nnpm run watch --workspace=packages/bruno-sqlite     # rebuild on change\nnpm run test  --workspace=packages/bruno-sqlite     # jest; sets --experimental-sqlite for you\n```\n\n`npm run dev` does **not** rebuild this package, and bruno-electron consumes its `dist/` — after\nediting anything under `packages/bruno-sqlite`, rebuild or run the watcher or the app keeps the old\nstatements. Run the tests through the npm script: bare `jest` misses the\n`NODE_OPTIONS=\"--experimental-sqlite\"` the repo's Node needs for `node:sqlite`.\n\n## Before you call it done\n\n- [ ] New persistence genuinely belongs here — not `.bru`/`.yml`, electron-store, or Redux\n- [ ] No merged migration edited; new schema added as a new migration with a working `down`\n- [ ] `npm run generate` output shows the expected statements\n- [ ] `migration:verify` run against a real `bruno.db` copy\n- [ ] Main-process callers try/catch `execute` / `transaction`, and still work when the database\n      is unavailable\n- [ ] Each statement the renderer needs has its own `ipcMain.handle` on\n      `datastore:<statement_file>:<statement_name>`; renderer access goes through a named hook\n- [ ] Package rebuilt (`npm run build:bruno-sqlite`) before testing in the app\n- [ ] `npm run test --workspace=packages/bruno-sqlite` plus the affected app/electron specs\n",".claude/rules/testing.md":"---\npaths:\n  - \"tests/**/*\"\n  - \"playwright/**/*\"\n  - \"playwright.config.ts\"\n---\n\n# Playwright E2E Testing Guide\n\n> **Reference:** For the full narrative guide — test structure, the page-module locator/action pattern, and worked examples — see [`docs/playwright-testing-guide.md`](../../docs/playwright-testing-guide.md). This file is the terse, path-scoped quick reference; the guide is the canonical source for conventions.\n\n## Running Tests\n\n```bash\nnpm run test:e2e                           # default + system-pac + mock-server (starts dev servers automatically)\nnpx playwright test tests/request/         # Run specific directory\nnpx playwright test --project=default      # Run specific project\nnpx playwright test --headed               # Watch mode\n```\n\nProjects: `default` (main), `system-pac` (depends on `default`), `mock-server`, `auth`, `ssl`.\n`npm run test:e2e` runs `default` + `system-pac` + `mock-server`; `auth` and `ssl` have their own\n`test:e2e:*` scripts and CI jobs. Config: `playwright.config.ts` — `fullyParallel: true`, `workers` unset\n(Playwright default, not single-worker), retries 0 local / 2 CI.\n\n## Test Fixtures (playwright/index.ts)\n\nSee `playwright/index.ts` for the current set; the table covers the common ones.\n\n| Fixture | Scope | Purpose |\n|---------|-------|---------|\n| `page` | test | Default page from `electronApp` |\n| `electronApp` | worker | Default Electron launch (skips onboarding) |\n| `launchElectronApp(opts)` | worker | Custom launch with userData, env, init data |\n| `reuseOrLaunchElectronApp(opts)` | worker | Caches app instances by key |\n| `pageWithUserData` | test | Loads `init-user-data/` from test dir, waits for app ready |\n| `createTmpDir(tag?)` | worker | Temp directory, auto-cleaned after worker |\n| `collectionFixturePath` | test | Copies `fixtures/collection(s)/` to temp dir |\n| `restartApp(opts)` | test | Restart with fresh user data |\n| `newPage` | test | Fresh app instance with tracing |\n| `installFakeClipboard(page)` | test | Replaces `navigator.clipboard` on that page for the test; `copiedText()` returns what the app copied |\n\n## Test Data Conventions\n\n- `tests/<suite>/init-user-data/` — Seed data for existing-user tests (`pageWithUserData`)\n- `tests/<suite>/init-user-data-fresh/` — Seed data for new-user tests (hasLaunchedBefore: false)\n- `tests/<suite>/fixtures/collection(s)/` — Collection fixtures auto-copied to temp dirs\n- `initUserDataPath` files support `{{projectRoot}}` template variable\n- No `initUserDataPath` → default prefs with `hasLaunchedBefore: true` (skip onboarding)\n- `dotEnv` overrides defaults (e.g., `DISABLE_SAMPLE_COLLECTION_IMPORT: 'false'`)\n- Adding/changing a key in the app's `preferences.json` defaults? Mirror it in the `defaultPreferences` mock in `playwright/index.ts` — see [the guide](../../docs/playwright-testing-guide.md#3-keep-defaultpreferences-in-sync-with-app-preferences).\n\n## Test Helpers (tests/utils/)\n\nReuse existing helpers instead of hand-rolling clicks; read the module for its current exports\n(e.g. `grep \"^export\" tests/utils/page/actions.ts`). Modules by responsibility:\n\n- `page/actions.ts` — high-level user actions (create/open collections, requests, folders, environments; send/save requests; workspace + tab management; import)\n- `page/locators.ts` — locator builders (`buildCommonLocators(page)` + per-protocol/feature variants) and table helpers\n- `page/runner.ts` — collection/folder runner actions and result assertions\n- `tests/utils/cli.ts` — `runCLI(cwd, args)` for bruno-cli testing\n- `tests/utils/wait.ts` — polling helpers (e.g. `waitForPredicate(predicate, {tries, interval})`)\n\n**Convention:** a spec must never inline raw selectors. Each `page/*` module owns one UI section and exports both a locator builder and its actions; a new section gets a new file whose builder is mapped into `buildCommonLocators`. Full pattern + examples in [the guide](../../docs/playwright-testing-guide.md#1-centralize-locators-and-actions-in-page-modules).\n\n## Waiting for App Ready\n\n```javascript\nawait page.locator('[data-app-state=\"loaded\"]').waitFor();\n```\n\nThis attribute is set when `main:app-loaded` IPC arrives (independent of `renderer:ready` chain).\n\n## Mutating Fixtures\n\nThe suite must be isolated (unique tmp paths, no shared state).\n\nMost specs load fixtures via `collectionFixturePath` (copies `fixtures/collection(s)/` to a temp dir) or `pageWithUserData` (loads `init-user-data/` into a fresh temp userData dir). These operate on **temp copies** — they need **no** cleanup, even though they mutate files on disk (e.g. persistence specs writing to `environments/*.bru` or `collection.bru`). Only a spec that mutates a **committed** fixture **in place** must restore it in `afterAll` — those use `git checkout <fixturePath>`. Don't flag a missing `afterAll` on a temp-copy spec.\n\n## Common Pitfalls\n\n1. **Worker-scoped fixtures persist across test retries** — app state from failed attempts carries over. If test creates resources (folders, collections), retries may fail trying to create duplicates.\n\n2. **Collapsed folders hide children from DOM** — `toHaveCount`/`toBeVisible` won't find items inside collapsed folders. Expand folders before asserting on their children.\n\n3. **Sequential tests sharing state** — Tests in the same `describe` that depend on prior test state will cascade-fail. The `afterAll` cleanup only runs once, not between retries.\n\n4. **File watcher timing** — After creating files via IPC, the collection watcher must detect and process the change before the UI updates. Use Playwright's auto-retrying assertions (e.g., `expect(locator).toBeVisible()`) rather than immediate checks.\n\n5. **Default env vars in fixture** — `launchElectronApp` sets `DISABLE_SAMPLE_COLLECTION_IMPORT: 'true'` by default. Tests that need onboarding must override via `dotEnv`.\n\n6. **The renderer outlives the test** — `electronApp` is worker-scoped and `page` hands back the same window, so anything a spec patches into the page (a global, a stubbed browser API) leaks into later specs in that worker. Use a fixture that restores on teardown; the real clipboard is also shared across workers, so assert copies through `installFakeClipboard(page)`, not `navigator.clipboard.readText()`.\n\n## Test Coverage Areas\n\nTest suites live under `tests/`, one directory per feature area — list that directory for the\ncurrent set. Examples: `request/`, `collection/`, `environments/`, `runner/`,\n`scripting/`, `graphql/`, `grpc/`, `websockets/`, `auth/`, `ssl/`.\n",".claude/skills/code-review/SKILL.md":"---\nname: code-review\ndescription: Review a Bruno diff, PR, or branch via focused reviewers run in parallel —\n  correctness, security, DSL, React, cross-platform, tests. Mirrors the CodeRabbit /\n  CODING_STANDARDS review.\n---\n\n# Reviewing Bruno code\n\nThis skill mirrors the automated CodeRabbit review (`.coderabbit.yaml`) so you can run\nthe same review locally. Source-of-truth order: coding standards → `CODING_STANDARDS.md`;\narchitecture/behavior → `.claude/rules/*`. `.coderabbit.yaml` mirrors these for CI parity —\nread it for a path instruction not summarized here, but the rules win if they ever disagree.\n\nThe review is **split into focused lenses that run in parallel**, so a large diff is\ncovered faster and each reviewer stays sharply scoped to one concern. Each lens lives in\nits own file under `reviewers/`. You (the orchestrator) dispatch the reviewers, then\nmerge and report their findings.\n\n## How to review (orchestration)\n\n1. **Get the diff.** Review only what changed — never untouched code. Pick the mode:\n   - **Committed range** (default): `git diff main...HEAD` against the base branch\n     (`main` or `release/*`). Update the base first (`git fetch` and confirm the local\n     base is current) — a stale base inflates the diff with already-merged, unrelated\n     changes and wastes a full fan-out. Each reviewer re-runs this scoped to its own\n     globs; since the range is pinned to fixed commits they all see identical bytes, so\n     there's no need to snapshot.\n   - **Working tree / uncommitted changes** (when asked to review unstaged or staged\n     work): the tree can shift mid-review, so re-running `git diff` per reviewer risks\n     each seeing a different snapshot. Instead, capture the diff **once** to a scratch\n     file and hand every reviewer that path. `git diff HEAD` covers staged + unstaged\n     tracked changes; run `git add -N .` first (reversible with `git reset`) so any new\n     untracked files also show up:\n     ```bash\n     git add -N . && git diff HEAD > \"$SCRATCH/review.diff\"\n     ```\n     `$SCRATCH` is your environment's scratchpad dir. Reviewers read this frozen diff for\n     their globs plus the on-disk files for surrounding context — the working-tree files\n     already hold the uncommitted state.\n2. **Enumerate changed files** — `git diff --name-only main...HEAD` (committed range) or\n   `git diff --name-only HEAD` (working tree). Use this to skip any lens whose file scope\n   isn't touched (e.g. no `packages/bruno-app/**` change → skip `react.md`; no `tests/**`\n   change → skip `e2e-tests.md`). Never skip the lenses scoped to all files, and never skip\n   `feature-parity.md` for a small diff — a one-file change is exactly the shape it catches.\n3. **Fan out the reviewers in parallel.** In a *single message*, launch one `Agent`\n   (subagent_type `Explore` or `general-purpose`) per in-scope reviewer below. Give each\n   subagent this exact briefing:\n   - The diff source — the committed range (e.g. `main...HEAD`) or the snapshot file path\n     (`$SCRATCH/review.diff`) — and the file globs it owns (from the reviewer file's\n     \"Scope\" line). For a snapshot, tell the reviewer to read that file for its globs\n     rather than re-run `git diff`.\n   - \"Read `.claude/skills/code-review/reviewers/_contract.md` for the shared persona and\n     output contract, then read `.claude/skills/code-review/reviewers/<file>` — and any rule\n     or source file it points to (e.g. `CODING_STANDARDS.md`, `.claude/rules/*.md`), which\n     hold the detailed checklist. Apply **only** that lens to the changed files in your\n     scope, at the severities the reviewer specifies. Do not review outside your scope.\"\n   Reviewers are read-only and independent — they don't coordinate, and overlap between\n   lenses is fine (you dedupe at merge time).\n4. **Merge and report.** Collect every reviewer's findings, drop exact duplicates, and\n   when two lenses flag the same `file:line` keep the higher severity. Regroup **by\n   file**, each finding tagged by severity (blocker / suggestion / nit) with `file:line`.\n   If the review request carries a problem statement or acceptance criteria (e.g. passed\n   as args), reconcile its enumerated deliverables — docs, migration notes, a test per new\n   default/branch — against the diff and flag any that are absent. If nothing's wrong, say\n   so briefly — don't manufacture nits.\n\n## Reviewers\n\nEach file is a self-contained checklist for one lens:\n\n| Reviewer file | Lens | Scope |\n|---|---|---|\n| `reviewers/correctness.md` | Correctness & root-cause | all source (excl. `tests/**`) |\n| `reviewers/architecture.md` | Architecture & dependency boundaries | `packages/**` |\n| `reviewers/conventions.md` | Coding standards & readability | all files |\n| `reviewers/react.md` | React — app files | `packages/bruno-app/**` |\n| `reviewers/cross-platform.md` | Cross-platform (macOS/Windows/Linux) | all files |\n| `reviewers/security.md` | Security & data safety | all source (excl. `tests/**`) |\n| `reviewers/dsl-changes.md` | On-disk DSL & serialization (backward compat) | `bruno-app`, `bruno-electron`, `bruno-cli`, `bruno-lang`, `bruno-filestore`, `bruno-schema(-types)`, `bruno-converters` |\n| `reviewers/feature-parity.md` | Co-related features & parity (app↔CLI, shim, converter, level, protocol twins) | `packages/**` |\n| `reviewers/e2e-tests.md` | Playwright E2E tests | `tests/**` |\n\n## Shared reviewer persona & output contract\n\nDefined once in `reviewers/_contract.md` — the small file every reviewer reads (dispatched\nreviewers read it, not this orchestration file). Keep the persona and the\n`<blocker|suggestion|nit> | <file>:<line> | <finding>` output shape there, not duplicated here,\nso a change updates a single place.\n",".claude/skills/code-review/reviewers/_contract.md":"# Shared reviewer persona & output contract\n\nRead by every `code-review` reviewer. (Orchestration lives in `../SKILL.md`; the per-lens\nchecklists live in the sibling `*.md` files.)\n\n**Persona** — an expert reviewer in TypeScript, JavaScript, Node.js, and Electron on an\nenterprise team. Be **concise**: one clear sentence per finding; elaborate only when asked.\nReview to the project's standard regardless of who authored or requested the change — never\nsoften severity for assumed intent or seniority. Ground every finding in the actual code: trust\nthe repo over any doc, guide, or comment when they disagree, and never cite a line or invent an\nexample value you haven't verified in source.\n\n**Output contract** — return a flat list, one finding per line:\n\n```\n<blocker|suggestion|nit> | <file>:<line> | <one-sentence finding>\n```\n\nReturn `no findings` (nothing else) when the scope is clean. Never invent nits to fill the list.\n",".claude/skills/code-review/reviewers/architecture.md":"# Architecture & dependency-boundaries reviewer\n\n**Scope:** `packages/**`.\n\nAdopt the reviewer persona and return findings in the output contract defined in\n`_contract.md`.\n\nReview the diff against **`.claude/rules/architecture.md`** — read its \"Dependency direction &\nownership boundaries\" section, plus \"Declared dependencies must match real imports\" when the diff\ntouches a `package.json`. The `@usebruno/*` packages form a strict dependency DAG and some carry hard\nplatform constraints. Report violations with `file:line`, severity:\n\n- **blocker** — a new internal `@usebruno/*` dependency that points *upward* or forms a cycle (a\n  shared lib importing `@usebruno/app` or `@usebruno/electron`); `bruno-common` importing a Node\n  built-in (`fs`/`path`/`os`/`crypto`/`child_process`/`node:*`) or any other `@usebruno/*` package\n  (it must stay browser-safe and dependency-free); `bruno-js` importing `electron`/IPC (it also runs\n  in the CLI); a runtime import of `bruno-schema-types` (types-only).\n- **suggestion** — code placed in the wrong package/layer: renderer-only logic in a shared lib,\n  host/Electron wiring pushed into `bruno-js`, or a util added to `bruno-common` that needs another\n  package; a new shared dependency that would be better kept local.\n- **suggestion** — manifest drift breaking the rule's **Declared dependencies must match real\n  imports**, read against the importing package's own `package.json`.\n- **Not a finding:** dependency edges already in the DAG, or a new *downward* dependency that fits\n  the established direction.\n",".claude/skills/code-review/reviewers/conventions.md":"# Coding standards & readability reviewer\n\n**Scope:** all changed files (`**/*`).\n\nAdopt the reviewer persona and return findings in the output contract defined in\n`_contract.md`.\n\nReview the diff against **`.claude/rules/conventions.md`** (read it; it points to\n`CODING_STANDARDS.md`, the code-guidelines source of truth). Report each violation with\n`file:line`, severity:\n\n- **suggestion** — readability problems from the rule: unclear or misleading names, unnecessary\n  abstraction (indirection that earns no readability or reuse), single-line indirection, `?.` where\n  the null case isn't handled right there, needless whitespace/diff churn, or missing comments on\n  genuinely complex flow.\n- **suggestion** — any breach of the rule's **Reuse before you write** or **Replacing code leaves\n  nothing behind** sections; confirm what the new code actually renders or reads before calling a\n  leftover dead. From **Before you call it done**, report only what the diff itself shows: a new\n  branch or acceptance criterion with no test, or a changed return/payload shape whose assertions\n  elsewhere weren't updated. Whether the author ran the suite isn't visible here — don't guess at it.\n- **nit** — pure style/formatting/naming deviations (indent, quotes, semicolons, trailing commas,\n  arrow parens, casing) — most are auto-fixed by ESLint, so keep these brief.\n",".claude/skills/code-review/reviewers/correctness.md":"# Correctness & root-cause reviewer\n\n**Scope:** all changed source (`**/*`, excluding `tests/**`).\n\nAdopt the reviewer persona and return findings in the output contract defined in\n`_contract.md`.\n\nValidate that the change is correct and, for every bug fix, that it addresses the\nunderlying problem rather than the visible symptom. Surface-level patches that mask a\ndefect without resolving it are a **blocker**:\n\n- Understand *why* the issue exists before accepting the fix — trace the bug to its origin,\n  don't stop at where it surfaced.\n- Flag patches that suppress a symptom (extra null guards, try/catch swallowing, defensive\n  re-checks, retries, timeouts, clamping values) while leaving the real cause in place. Ask:\n  if this defends against a bad value, *where does the bad value come from*, and should it be\n  fixed there instead?\n- A fix at the wrong layer (UI guard for a data-layer bug, caller working around a callee's\n  contract violation) is a symptom patch — call out the correct layer.\n- Watch for fixes scoped to one reproduction case when the same root cause can manifest\n  elsewhere; the correct fix usually covers all call sites, not just the reported one.\n- When a fix looks like a workaround, say so and name the deeper change that would actually\n  resolve it — even if the larger fix is out of scope, the PR should acknowledge it.\n- Beyond bug fixes, check ordinary correctness: off-by-one and boundary errors, unhandled\n  promise rejections / missing `await`, swallowed errors, incorrect null/undefined handling,\n  and edge cases the change introduces.\n- **Check the twin path.** Bruno keeps parallel implementations of the same behavior — `.bru`\n  vs `.yml` serializers, the default app-data workspace vs custom-filesystem workspaces, the\n  Electron request pipeline vs its `bruno-cli` copy, the scripting API vs its QuickJS shim,\n  `bruno-filestore` vs the `bruno-converters` export. A change that touches one path must be\n  verified against its twin; behavior that diverges between them (a value that persists or\n  defaults in one but is dropped or defaulted differently in the other) is a bug, not two\n  independent features. The full map of which surfaces pair up is\n  `.claude/rules/feature-parity.md`; the `feature-parity` lens reviews it in depth, so raise\n  only the correctness consequence here and leave the enumeration to that lens.\n- **`x || default` on a field whose absence is meaningful.** When \"not set\" / \"never\n  configured\" is a distinct state, falsy-coalescing (`version || '1'`) fabricates a value for\n  the unset case, erases the distinction, and often diverges from a sibling path that handles\n  it correctly. Flag it; use `??` or an explicit `undefined` check when the unset state must\n  survive.\n",".claude/skills/code-review/reviewers/cross-platform.md":"# Cross-platform reviewer\n\n**Scope:** all changed files (`**/*`).\n\nAdopt the reviewer persona and return findings in the output contract defined in\n`_contract.md`.\n\nReview the diff against **`.claude/rules/cross-platform.md`** (read it) — Bruno ships on macOS,\nWindows, and Linux. Report each violation with `file:line`, severity:\n\n- **blocker** — anything that breaks a platform: hardcoded `/` or `\\` separators instead of\n  `path.join`/`path.resolve`; hardcoded paths (`/home/`, `C:\\Users\\`, `~/`) instead of\n  `os.homedir()`/`app.getPath()`; platform-specific shell/`child_process` without a fallback\n  (`which` vs `where`, `spawn` needing `shell:true` on Windows); Unix-only signals; `/tmp`\n  instead of `os.tmpdir()`.\n- **suggestion** — portable-but-fragile patterns not yet a concrete break: case-sensitivity\n  assumptions, inconsistent CRLF/LF handling, `fs.chmod`/`fs.access` assuming Unix permission bits.\n- **suggestion** — multiline `.bru`/text-block parsing that splits on `\\n` instead of a\n  CRLF-aware regex (`/\\r\\n|\\r|\\n/`) — see the rule's Line Endings section.\n",".claude/skills/code-review/reviewers/dsl-changes.md":"# On-disk DSL & serialization reviewer\n\n**Scope:** `packages/bruno-app/**`, `packages/bruno-electron/**`, `packages/bruno-cli/**`,\n`packages/bruno-lang/**`, `packages/bruno-filestore/**`, `packages/bruno-schema/**`,\n`packages/bruno-schema-types/**`, `packages/bruno-converters/**`.\nThe format packages own serialization, but DSL objects are assembled in `bruno-app` (Redux)\nand `bruno-electron` (IPC), and `bruno-cli` reads/writes these files directly — a shape change\nmade there won't be caught by the serialization layer, so they're all in scope.\n\nAdopt the reviewer persona and return findings in the output contract defined in\n`_contract.md`.\n\nReview the diff against **`.claude/rules/dsl-changes.md`** (read it) — Bruno persists\ncollections/requests/environments/config as `.bru` and `.yml` files read across app versions, so\nbackward compatibility is the priority. Report violations with `file:line`, severity:\n\n- **blocker** — a breaking field change (rename, remove, retype, repurpose, or changed\n  default/semantics); a new field that isn't optional with a safe default; format drift between\n  `bru` and `yml` (or between parse and stringify); a lossy round-trip (`stringify` drops\n  unknown fields, or `parse(stringify(x)) !== x`); a shape change with no read-time compat shim;\n  a cryptic, abbreviated, or inconsistent property name (names are permanent and user-facing).\n- **blocker** — new `.bru` *syntax* (escape form, block delimiter, annotation grammar) that older\n  parsers can't read; an escaping/delimiter change not proven by on-disk reparse\n  (`parse(read(serialize(x)))`) or that escapes a multi-char delimiter by splitting on the whole\n  delimiter. See the rule's escaping and forward-compat sections.\n- **suggestion** — missing round-trip test in both formats or missing old-format golden fixture;\n  a name/structure that won't scale (positional vs keyed, widening union).\n- Also push back on any **unnecessary** DSL change that could be solved in memory / UI without\n  altering the serialized shape.\n- **Not a finding:** the read-accepts-`{content}` / write-emits-`string` asymmetry on\n  `description` fields — string-only internally is an established convention (see rule), not a\n  lossy round-trip.\n",".claude/skills/code-review/reviewers/e2e-tests.md":"# E2E tests reviewer\n\n**Scope:** files under `tests/**` (`path: 'tests/**/**.*'`).\n\nAdopt the reviewer persona and return findings in the output contract defined in\n`_contract.md`.\n\nReview the diff against **`.claude/rules/testing.md`** and the e2e checklist in\n**`.coderabbit.yaml`** (read both). Report violations with `file:line`, severity:\n\n- **blocker** — `test.only`; `page.pause()`.\n- **suggestion** — `page.waitForTimeout()` where an `expect()` locator assertion could wait\n  instead; inline raw selectors instead of `tests/utils/page/*` modules; a spec that mutates a\n  committed fixture without restoring it in `afterAll`; shared state / non-isolated tmp paths.\n- **suggestion** — a non-discriminating assertion: `toContain('description:')` when the fixture\n  already carries that substring on unrelated rows passes even if the change under test never\n  happened; assert the unique value actually written by this change.\n- **suggestion** — parallel `.bru` and `.yml` fixtures for the same scenario that disagree\n  field-for-field (e.g. a row `selected: false` in one, enabled in the other) — a fixture bug\n  even when the current assertion tolerates it.\n- **nit** — locators not stored in variables; a single broad assertion where several focused\n  ones fit; steps not wrapped in `test.step`.\n",".claude/skills/code-review/reviewers/feature-parity.md":"# Co-related features & parity reviewer\n\n**Scope:** `packages/**`.\n\nAdopt the reviewer persona and return findings in the output contract defined in\n`_contract.md`.\n\nReview the diff against **`.claude/rules/feature-parity.md`** (read it). This lens asks one\nquestion of every changed file: **what else had to change with it, and did it?** For each changed\nfile, find it in the rule's tables, open the counterpart, and check whether the counterpart logic\nis in the diff. Report with `file:line`, severity:\n\n- **blocker** — logic changed on one side of an app ↔ CLI pair (§1) when the twin file holds the\n  counterpart logic and the diff leaves it untouched. Includes bug fixes in `bruno-electron` whose\n  root cause `bruno-cli` also has a copy of. Name the twin path and function.\n- **blocker** — a scripting API added or changed in `bruno-js/src/{bru,bruno-request,bruno-response}.js`\n  without the matching bridge in `bruno-js/src/sandbox/quickjs/shims/`.\n- **blocker** — a persisted field that skips a §2 layer: missing from `bruno-schema` (throws on\n  save), the app save transform, one on-disk format, the BRU → YML migration, or OpenCollection\n  import/export (either direction). Flag the missing surface only — the on-disk contract itself\n  (compat, escaping, round-trip) belongs to the `dsl-changes` lens; don't duplicate its findings.\n- **suggestion** — identical logic patched in both runtimes that has no Electron- or CLI-only\n  dependency and could move into `@usebruno/requests` / `@usebruno/common` (`utils/cookies.js` is\n  the precedent); otherwise, a missing comment naming the pairing at both sites.\n- **suggestion** — a new app runner option with no `bru run` flag; a feature at one scope level\n  with no decision for the others or `inherit`; a wire-request change without a\n  `snippet-generator.js` update; an HTTP-only change on a path gRPC/WebSocket also flow through;\n  a Postman/Insomnia/OpenAPI converter not updated for a field its format can represent.\n- **suggestion** — tests on one side of a pair only.\n\n**Not a finding:** a stated asymmetry, or one the rule lists as known-divergent (no preferences\nstore / Electron window / system-browser OAuth / Yup validation in the CLI; CLI runner handles\nonly `http-request` and `graphql-request`). A change to runtime-specific code in a paired file\n(e.g. an Electron-only helper in `utils/filesystem.js`) is not a gap. Never claim a missing twin\nwithout confirming the twin file exists and holds the counterpart logic.\n",".claude/skills/code-review/reviewers/react.md":"# React — app files reviewer\n\n**Scope:** `packages/bruno-app/**`.\n\nAdopt the reviewer persona and return findings in the output contract defined in\n`_contract.md`.\n\nReview changed components against **`CODING_STANDARDS.md` §React** (read it); for changes that\ntouch the store, also consult **`.claude/rules/redux-store.md`**. Report violations with\n`file:line`, severity:\n\n- **blocker** — an `useEffect` that could be derived state, an event handler, or a custom hook;\n  a hardcoded hex/rgb/hsl/named color instead of the styled-components `theme` prop (breaks the other\n  12 themes — verify the token path exists on the theme object); namespaced hook import (`React.useX`);\n  a component that mixes controlled and uncontrolled state; a hook called after a conditional\n  early return.\n- **blocker** — anything breaking **`.claude/rules/bruno-app-layout.md`** (read it),\n  component-specific vs shared placement included; the drift noted at the end of that file is not a\n  finding.\n- **blocker** — a new theme token defined in `themes/light|dark/*.js` without the matching property\n  added to `themes/schema/oss.js`, or added to the schema but missing from some of the 13 theme files.\n  The schema is `additionalProperties: false` and `providers/Theme/index.js` validates against it at\n  runtime, so either gap invalidates that whole theme and silently falls the user back to the default\n  with an error toast.\n- **suggestion** — a missing memo that breaks a dependency array or re-renders a heavy child, or\n  a gratuitous memo wrapping a cheap primitive; Tailwind used for colors (layout only);\n  a testable element without `data-testid`; a theme-token swap resolving to a different value than\n  the literal it replaced, or one value left hardcoded while its neighbours were themed (resolve it\n  in `themes/` first); a `document`/`window` listener not gated on the state that makes it relevant,\n  even where an incidental detail makes it harmless today.\n- **suggestion** — a new `EditableTable` column that omits the `readOnly`/`editMode` gating its\n  sibling columns carry (`readOnly={column.readOnly}` at render): the field stays editable in\n  view / read-only mode even though the change handler discards the edit.\n- **suggestion** — optimistic success state (`copied`, `saved`, `done`) set unconditionally\n  rather than gated on the operation resolving — e.g. `copied = true` after an optional-chained\n  `navigator.clipboard?.writeText` that no-ops in an insecure context — showing a false\n  confirmation.\n",".claude/skills/code-review/reviewers/security.md":"# Security & data safety reviewer\n\n**Scope:** all changed source (`**/*`, excluding `tests/**`).\n\nAdopt the reviewer persona and return findings in the output contract defined in\n`_contract.md`.\n\nBruno is an offline API client that handles user credentials, tokens, and runs\nuser-authored scripts in a sandbox. Review changes for these Bruno-specific risks:\n\n- **No secret leakage.** Auth tokens, passwords, API keys, OAuth2 secrets, and env-var\n  values must never be written to logs, console, error messages, or telemetry. A secret\n  logged in cleartext is a **blocker**. Watch for `console.log`/logger calls that dump a\n  whole request, header set, or config object containing credentials.\n- **Script sandbox integrity** (`bruno-js`, QuickJS / Node VM). Changes that widen the\n  sandbox surface — exposing new Node built-ins, `require`, filesystem, or `process` to\n  user scripts, or passing unsanitized script output back into privileged code — are a\n  potential sandbox escape; call them out.\n- **IPC input validation** (`bruno-electron` handlers, `preload.js`). Treat every argument\n  arriving from the renderer as untrusted: validate file paths (guard against traversal\n  outside the collection dir), types, and bounds before acting on them.\n- **Path traversal & arbitrary writes.** File reads/writes derived from user or request\n  data must be confined to the intended directory — no `../` escape, no writing outside the\n  collection root.\n- **Injection & unsafe eval.** Flag string-built shell commands, `eval`, or dynamic\n  `Function` on untrusted input, and unescaped interpolation into commands or queries.\n- **Dependency & network surface.** Note new runtime dependencies or outbound network calls\n  that don't fit Bruno's offline-first, privacy-first posture.\n\nKeep findings concrete — tie each to how the tainted value reaches the sink.\n",".claude/skills/write-e2e-test/SKILL.md":"---\nname: write-e2e-test\ndescription: Write a Playwright E2E test for Bruno. Use when adding or modifying\n  tests under tests/, creating a new test suite, or reproducing a bug as an\n  e2e spec.\n---\n\n# Writing a Bruno E2E test\n\nRead `.claude/rules/testing.md` first for the full fixture and helper reference. See\n`docs/playwright-testing-guide.md` for the extended walkthrough (codegen, running,\ndebugging, troubleshooting) — but where it disagrees with this skill or the actual repo,\ntrust the repo: specs live in `tests/` (not `e2e-tests/`), the suite is `fullyParallel`,\nand projects are `default`, `system-pac`, `auth`, and `ssl`\n(verify in `playwright.config.ts`).\n\n## Start with codegen or by hand\n\n- **Codegen** (fastest for a new flow): `npm run test:codegen <name>` launches the app,\n  records your clicks, and writes `tests/<name>.spec.ts`. Treat the output as a rough\n  draft — move it into the right area folder, swap generated selectors for `data-testid`\n  or helpers, wrap steps in `test.step`, and delete dead recorded actions.\n- **By hand**: `import { test, expect } from '../../playwright'` and follow the steps below.\n\n## Steps\n\n1. **Place the spec** at `tests/<area>/<name>.spec.ts` — reuse an existing area dir under\n   `tests/` (list it for the current set; e.g. `request/`, `collection/`, `environments/`,\n   `runner/`, `scripting/`) or add one. Pick the project: `default` for most specs;\n   `auth` / `ssl` / `system-pac` only when the test needs those\n   servers or external dependencies.\n2. **Use fixtures** from `playwright/index.ts` (read it for the full set) instead of launching\n   the app yourself:\n   - `page` / `electronApp` — default app with onboarding skipped.\n   - `pageWithUserData` — seeds data from `tests/<area>/init-user-data/`\n     (use `init-user-data-fresh/` for new-user flows).\n   - `createTmpDir(tag?)` — throwaway dir for collections/files the test creates; cleaned\n     up automatically, so prefer it over hardcoded paths.\n   - `newPage`, `launchElectronApp`, `reuseOrLaunchElectronApp`, `restartApp` — for\n     multi-window or app-restart scenarios.\n   - `collectionFixturePath` / `workspaceFixturePath` — copy a committed fixture to a tmp\n     dir and get its path.\n3. **Use helpers** from `tests/utils/page/` instead of hand-rolling clicks — read the module\n   for the current set (e.g. `createCollection`, `sendRequest`, `runCollection`).\n4. **Structure each test** arrange → act → assert, and wrap the phases in `test.step` so\n   the HTML report reads clearly. Prefer several focused assertions over one broad check.\n5. **Wait for readiness**: `await page.locator('[data-app-state=\"loaded\"]').waitFor()`\n   before asserting on app state.\n6. **Locate** by `data-testid` (Bruno's dominant convention) or documented classes\n   (`.collection-item-name`); stable `getByRole` / `getByText` locators are fine too.\n   Avoid brittle CSS, `#id`, or index-based selectors. If no stable selector exists, add a\n   `data-testid` to the component rather than locating by text.\n7. **If the test mutates a committed fixture**, restore it in `afterAll`. Existing specs\n   do this with `git checkout <fixturePath>` (e.g. `tests/response-examples/*.spec.ts`) —\n   follow the pattern already used by neighbouring specs in that area.\n8. **Use auto-retrying assertions** (`expect(locator).toBeVisible()`); never assert\n   immediately after a file-watcher-triggered change (created files take a tick to appear).\n   Reserve `page.waitForTimeout()` for when no locator assertion can wait instead.\n9. **Run it**: `npx playwright test tests/<area>/<name>.spec.ts --project=default`\n   (the web + test servers start automatically). Debug with `--ui`, `--headed`, `--debug`,\n   or `--trace on` (then `npx playwright show-trace test-results/**/trace.zip`).\n\n## Checklist before done\n\n- [ ] Test is isolated — unique tmp paths (`createTmpDir`), no state shared with other tests\n- [ ] Passes on a clean second run (retry-safe; worker fixtures persist across retries)\n- [ ] Folders expanded before asserting on their children (collapsed = not in DOM)\n- [ ] Any mutated committed fixture is restored in `afterAll`\n- [ ] Any codegen-generated draft refactored to fixtures/helpers + stable selectors\n- [ ] Steps wrapped in `test.step`; no `test.only`, no `page.pause()`\n","packages/bruno-app/storybook/README.md":"# Storybook\n\nStorybook is our workbench and living documentation for the `bruno-app` UI — primarily the design-system primitives under `src/ui`. Use it to build a component in isolation, see every state at once, and check how it looks across Bruno's themes.\n\n## Running\n\nFrom the repo root:\n\n```bash\nnpm run storybook --workspace=packages/bruno-app        # dev server on http://localhost:6006\nnpm run build-storybook --workspace=packages/bruno-app  # static build -> storybook-static/\n```\n\nConfig lives in this folder: `main.js` (framework, addons, stories glob, webpack aliases) and `preview.jsx` (global decorators, the theme toolbar).\n\n## Where stories live\n\nStories are co-located next to the component they document and picked up by the glob in `main.js`:\n\n```text\nsrc/**/*.stories.@(js|jsx|mjs|ts|tsx)\n```\n\nSo `src/ui/Button/index.js` is documented by `src/ui/Button/Button.stories.jsx`. `main.js` mirrors the app's path aliases (`ui`, `components`, `themes`, `providers`, `utils`, …), so imports like `import Button from 'ui/Button'` work inside stories.\n\n## Writing a story\n\nStandard CSF. Give the component a `Components/<Name>` title and tag it `autodocs` so it gets a Docs page:\n\n```jsx\nimport Button from './index';\n\nexport default {\n  title: 'Components/Button',\n  component: Button,\n  parameters: { layout: 'centered' },\n  tags: ['autodocs'],\n  argTypes: {\n    variant: { control: 'select', options: ['filled', 'outline', 'ghost'] }\n  }\n};\n\nexport const Default = { args: { children: 'Button' } };\n```\n\nTheming is automatic — `preview.jsx` wraps every story in the styled-components `ThemeProvider` (and the app's `ThemeContext`), so components read `props.theme` and `useTheme()` normally. Don't add your own theme provider.\n\n## Theme switching\n\nThe toolbar has a **Theme** picker listing all of Bruno's registered themes (from `themes/index.js` `themeRegistry`). Selecting one re-renders the current story with that theme's real tokens and sets the preview's light/dark mode.\n\nNote: components styled via styled-components `props.theme` (nearly all of `src/ui`) get full per-theme accuracy. Anything relying on raw `var(--color-*)` CSS variables only toggles light vs dark, since those variables aren't per-theme.\n\n## Conventions & gotchas\n\nThese are worth following so components render correctly on the **Docs** page, where every story renders inline in one shared document (unlike Canvas, which isolates each story in an iframe).\n\n**Keep the Docs description clean.** Storybook uses a component's leading JSDoc as the Docs description and does not strip `@param` tags, so a rich JSDoc block dumps as a run-on paragraph. Set an explicit description in the story instead of trimming the component's source comments:\n\n```js\nparameters: {\n  docs: { description: { component: 'One clean paragraph. See the props table below.' } }\n}\n```\n\n**Don't hardcode `name` on radio-based groups.** `RadioGroup`/`SegmentedControl` auto-generate a unique `name` per instance. On the Docs page the primary story renders twice and all stories share the DOM, so a hardcoded `name` makes separate instances collide into one native radio group (selecting in one unchecks another). Omit `name` unless you specifically want a shared native group.\n\n**Give overlay components room.** Dropdowns, menus, and modals open a popover that gets clipped by the short, `overflow`-hidden inline Docs preview. Render those stories in an iframe with a height:\n\n```js\nparameters: { docs: { story: { inline: false, height: '360px' } } }\n```\n\n**Always give group components an accessible name.** Components like `RadioGroup`/`SegmentedControl` warn in development if they render without `label`, `ariaLabel`, or `ariaLabelledBy` — pass one in stories and real usage. Don't fabricate a placeholder name to silence the warning; a meaningless label is worse than none (only the shared native `name` attribute is auto-generated, because it carries no human meaning).\n\n**Inline story state — don't wrap in a helper component.** For controlled components, put `useState` directly inside the story's `render` and return the component itself. Wrapping it in a `<Controlled>` helper makes the code panel show `<Controlled>` instead of the real component, because the generated source reflects the root element the `render` returns.\n\n```jsx\nexport const Default = {\n  render: (args) => {\n    const [value, setValue] = useState('http');\n    return <SegmentedControl {...args} value={value} onChange={setValue} items={items} />;\n  }\n};\n```\n\n**Demo wrappers, not component chrome.** If a story needs a sized/bordered container to demonstrate behavior (e.g. width for overflow), keep it clearly a demo wrapper so it isn't mistaken for the component's own styling.\n\n## Docs addon\n\nAutodocs (the Docs tab) requires `@storybook/addon-docs`, registered in `main.js` and pinned to the **same version** as the `storybook` core package — a version mismatch throws `docsParameter.renderer is not a function`. Run `npx storybook doctor` to check for mismatched Storybook packages.\n\n## Code panel\n\n`preview.jsx` turns on the source/code views that `@storybook/addon-docs` provides:\n\n```js\ndocs: {\n  codePanel: true,              // a \"Code\" tab in the Canvas addon panel\n  canvas: { sourceState: 'shown' } // Docs \"Show code\" expanded by default\n}\n```\n\nThe snippet reflects the root element your story's `render` returns, so follow the \"inline story state\" note above to keep it showing the real component. There's also a `</>` Show code button in the Canvas toolbar.\n\n## Adding a new primitive\n\n1. Build the component under `src/ui/<Name>/` (`index.js` + `StyledWrapper.js`), following the existing conventions: `$`-prefixed transient style props, theme tokens, a `dataTestId`, forwarded refs, and forwarded `...rest`.\n2. Add `<Name>.stories.jsx` next to it with a `Components/<Name>` title, `autodocs`, `argTypes`, a clean `docs.description.component`, and a story per meaningful state.\n3. Add `<Name>.spec.jsx` (React Testing Library) for behavior and accessibility.\n\n### Group primitives (single-select)\n\nSingle-select group primitives (`RadioGroup`, `SegmentedControl`) follow a specific layout: the implementation lives in a `components/` subfolder and only the data-driven **list** API is exposed publicly, keeping the compound pieces internal so a compound API can be surfaced later without a rewrite.\n\n```text\nsrc/ui/<Name>/\n├── components/\n│   ├── <Name>Base.jsx    # compound: owns the context + renders the radiogroup\n│   ├── <Option>.jsx      # the internal option (reads context)\n│   ├── <Name>.jsx        # public list component — maps `items` → options\n│   └── StyledWrapper.js  # styling\n├── index.jsx             # exposes only the list <Name> (default export)\n├── <Name>.stories.jsx\n└── <Name>.spec.jsx\n```\n\nConsumers use the list API: `<SegmentedControl value onChange items={[{ value, label, ... }]} />`. To expose the compound API later, re-export `<Option>` / `<Name>Base` from `index.jsx`.\n","packages/bruno-cli/docker/README.md":"# Bruno CLI Docker Images\n\nOfficial Docker images for [Bruno CLI](https://www.usebruno.com), enabling container-native API collection runs in CI/CD pipelines and local environments without requiring Node.js or npm on the host. See the [Bruno CLI docs](https://docs.usebruno.com/bru-cli/overview) for CLI usage.\n\n## Registries\n\n```bash\ndocker pull usebruno/cli:latest\ndocker pull ghcr.io/usebruno/cli:latest\n```\n\n---\n\n## Variants\n\n| Variant | Base image | Details |\n|---------|-----------|---------|\n| **Alpine** (default) | `node:22-alpine` | [→ Alpine README](https://github.com/usebruno/bruno/blob/main/packages/bruno-cli/docker/images/alpine/README.md) |\n| **Debian** | `node:22-slim` | [→ Debian README](https://github.com/usebruno/bruno/blob/main/packages/bruno-cli/docker/images/debian/README.md) |\n\n### Quick choice\n\n- **Use Alpine** unless you have a specific reason not to (90% of users)\n- **Use Debian** if you hit SSL/glibc compatibility issues\n\n---\n\n## Tags\n\nEvery release publishes the following tags to **both** Docker Hub (`usebruno/cli`) and GHCR (`ghcr.io/usebruno/cli`).\n\n### Alpine variant (default)\n\n| Tag pattern | Example | Notes |\n|-------------|---------|-------|\n| `latest` | `usebruno/cli:latest` | Newest release marked as latest. Only moves when the publish workflow is run with \"Tag this version as latest\" checked. |\n| `latest-alpine` | `usebruno/cli:latest-alpine` | Alias of `latest` — also alpine. |\n| `alpine` | `usebruno/cli:alpine` | Newest alpine, moves on every alpine publish. |\n| `<version>` | `usebruno/cli:3.3.0` | Exact version, immutable. |\n| `<version>-alpine` | `usebruno/cli:3.3.0-alpine` | Exact version, explicitly alpine. |\n| `<major.minor>` | `usebruno/cli:3.3` | Floats with patch releases (3.3.x). |\n| `<major.minor>-alpine` | `usebruno/cli:3.3-alpine` | Same, explicitly alpine. |\n| `<major>` | `usebruno/cli:3` | Floats with any 3.x.x release. |\n| `<major>-alpine` | `usebruno/cli:3-alpine` | Same, explicitly alpine. |\n\n### Debian variant\n\n| Tag pattern | Example | Notes |\n|-------------|---------|-------|\n| `latest-debian` | `usebruno/cli:latest-debian` | Newest debian release marked as latest (gated by the same checkbox). |\n| `debian` | `usebruno/cli:debian` | Newest debian, moves on every debian publish. |\n| `<version>-debian` | `usebruno/cli:3.3.0-debian` | Exact version, debian. |\n| `<major.minor>-debian` | `usebruno/cli:3.3-debian` | Floats with debian patch releases. |\n| `<major>-debian` | `usebruno/cli:3-debian` | Floats with any 3.x.x debian release. |\n\nThe unsuffixed tags (`:latest`, `:3.3.0`, `:3.3`, `:3`, `:alpine`) always resolve to the alpine variant by convention.\n\n---\n\n## Step-by-step guide\n\n### Step 1 — Pull the image\n\n```bash\n# latest (alpine by default — smallest, fastest to pull)\ndocker pull usebruno/cli:latest\n\n# specific version (recommended for production CI)\ndocker pull usebruno/cli:3.3.0\n\n# major.minor — gets patch updates automatically\ndocker pull usebruno/cli:3.3\n\n# debian variant\ndocker pull usebruno/cli:debian\ndocker pull usebruno/cli:3.3.0-debian\n```\n\n---\n\n#### Check it works\n\n```bash\ndocker run --rm usebruno/cli --version\n```\n\n---\n\n### Step 2 — Run your collection\n\n> These examples assume you are running `docker` from your Bruno collection directory. Mount that directory to `/bruno` and pass `bru` arguments directly after the image name. If your collection lives elsewhere on disk, see the path-based examples further down.\n\n> **Cross-platform note:** the examples below use `$(pwd)` which works in Bash / Zsh / Git Bash / WSL. On Windows native shells, substitute `$(pwd)` with `${PWD}` (PowerShell) or `%cd%` (CMD).\n\n> **Adding `bru` options:** The examples below show docker-specific flags. For all `bru run` options — recurse (`-r`), environments, variables, reporters, bail, and more — see the [Bruno CLI docs](https://docs.usebruno.com/bru-cli/overview).\n\n```bash\n# run every request in the Bruno collection (current dir)\ndocker run -v $(pwd):/bruno usebruno/cli run \n\n# run a specific subfolder (group of requests) within that collection\ndocker run -v $(pwd):/bruno usebruno/cli run ./api-tests\n\n# run a single .bru request file from that collection\ndocker run -v $(pwd):/bruno usebruno/cli run ./api-tests/login.bru\n\n# write a JUnit XML report (lands in the current directory because of the bind mount)\ndocker run -v $(pwd):/bruno usebruno/cli run --reporter-junit results.xml\n```\n\nFor Windows CMD users, swap `$(pwd)` with `%cd%`:\n\n```cmd\ndocker run -v %cd%:/bruno usebruno/cli run \n```\n\n#### Running a collection that lives at a different path\n\nIf your collection is not in your current directory, point `docker` at its path (relative or absolute) instead of `$(pwd)`:\n\n```bash\n# run every request in a collection at an arbitrary path\ndocker run -v /path/to/your/collection:/bruno usebruno/cli run\n\n# run a single .bru file from a collection at an arbitrary path\ndocker run -v /path/to/your/collection:/bruno usebruno/cli run ./auth/login.bru\n```\n\n> **Note on `--rm`:** Examples below include `--rm`. Docker keeps stopped containers around after they exit, which lets you `docker logs` or `docker inspect` them later for debugging. If you'd rather have Docker auto-delete the container as soon as `bru` finishes — useful for CI runs or to avoid `docker ps -a` filling up with stale entries — append `--rm` to any `docker run` (or `docker compose run`) command:\n\n```bash\ndocker run --rm -v $(pwd):/bruno usebruno/cli run\n```\n\n> It's purely a cleanup convenience; it doesn't affect the image, mounts, stdout output, or exit code.\n\n\n## CI/CD integration\n\n### GitHub Actions\n\n```yaml\njobs:\n  api-tests:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n\n      - name: Run Bruno collection\n        run: |\n          docker run --rm \\\n            -v ${{ github.workspace }}:/bruno \\\n            usebruno/cli:latest run --output results.xml --format junit\n\n      - name: Publish Test Report\n        uses: dorny/test-reporter@v3\n        if: success() || failure()\n        with:\n          name: Bruno Test Results\n          path: ${{github.workspace}}/results.xml\n          reporter: java-junit\n```\n\n### GitLab CI\n\n```yaml\napi-tests:\n  image: usebruno/cli:latest\n  script:\n    - bru run --output results.xml --format junit\n  artifacts:\n    reports:\n      junit: results.xml\n```\n\n---\n\n## Docker Compose\n\n### Quick example\n\nA minimal `docker-compose.yml` for running a Bruno collection alongside your project:\n\n```yaml\nservices:\n  bruno-cli:\n    image: usebruno/cli:latest\n    container_name: bruno-cli-runner\n    volumes:\n      - /path/to/collection:/bruno\n      - /path/to/reports:/reports\n    command:\n      run .\n      -r\n      --env ci\n      --reporter-json /reports/results.json\n      --reporter-junit /reports/results.xml\n      --reporter-html /reports/results.html\n```\n\nThen run:\n\n```bash\ndocker compose run bruno-cli\n```\n\nThe `/path/to/reports:/reports` mount catches the JSON, JUnit XML, and HTML reports on the host — drop any `--reporter-*` flag to skip that format.\n\n### Try it from this repo\n\nA ready-to-run `docker-compose.yml` lives in this repo at [`packages/bruno-tests/docker-compose.yml`](https://github.com/usebruno/bruno/blob/main/packages/bruno-tests/docker-compose.yml). It mounts the sibling `collection/` directory into the container, runs the `echo` folder against the `Prod` environment, and writes JSON, JUnit XML, and HTML reports into `packages/bruno-tests/reports/`:\n\n```bash\ncd packages/bruno-tests\ndocker compose run bruno-cli\n```\n\nThis fires a small set of requests against public endpoints that demonstrate the CLI executing requests and assertions inside a container.\n\n### Standalone demo\n\nFor a clone-and-run demo with a curated collection, see [`bruno-collections/bruno-cli-docker`](https://github.com/bruno-collections/bruno-cli-docker).\n\n---\n\n## Image details\n\nAll variants include:\n\n- **Entrypoint:** `bru`\n- **Working directory:** `/bruno`\n- **User:** `node` (UID 1000, non-root)\n- **Architectures:** `linux/amd64`, `linux/arm64`\n\n","packages/bruno-cli/docker/images/alpine/README.md":"# Bruno CLI — Alpine\n\nAlpine Linux variant of the Bruno CLI Docker image.\n\n**Base image:** `node:22-alpine`\n\n## Building\n\n```bash\ndocker build -t usebruno/cli:alpine ./images/alpine\n\n# with specific Bruno CLI version\ndocker build \\\n  --build-arg BRUNO_VERSION=3.3.0 \\\n  -t usebruno/cli:3.3.0-alpine \\\n  ./images/alpine\n```\n\n## Usage\n\n```bash\n# Run a collection\ndocker run -v $(pwd):/bruno usebruno/cli:alpine run\n\n# with pinned version\ndocker run -v $(pwd):/bruno usebruno/cli:3.3.0-alpine run\n```\n","packages/bruno-cli/docker/images/debian/README.md":"# Bruno CLI — Debian\n\nDebian slim variant of the Bruno CLI Docker image.\n\n**Base image:** `node:22-slim`\n\n## Building\n\n```bash\ndocker build -t usebruno/cli:debian ./images/debian\n\n# with specific Bruno CLI version\ndocker build \\\n  --build-arg BRUNO_VERSION=3.3.0 \\\n  -t usebruno/cli:3.3.0-debian \\\n  ./images/debian\n```\n\n## Usage\n\n```bash\n# Run a collection\ndocker run -v $(pwd):/bruno usebruno/cli:debian run \n\n# with pinned version\ndocker run -v $(pwd):/bruno usebruno/cli:3.3.0-debian run \n```\n","packages/bruno-cli/readme.md":"# Bruno CLI\n\nWith Bruno CLI, you can now run your API collections with ease using simple command line commands.\n\nThis makes it easier to test your APIs in different environments, automate your testing process, and integrate your API tests with your continuous integration and deployment workflows.\n\nFor detailed documentation, visit [Bruno CLI Documentation](https://docs.usebruno.com/bru-cli/overview).\n\n## Installation\n\nTo install the Bruno CLI, use the node package manager of your choice, such as NPM:\n\n```bash\nnpm install -g @usebruno/cli\n```\n\n## Getting started\n\nNavigate to the directory where your API collection resides, and then run:\n\n```bash\nbru run\n```\n\nThis command will run all the requests in your collection. You can also run a single request by specifying its filename:\n\n```bash\nbru run request.bru\n```\n\nOr run all requests in a collection's subfolder:\n\n```bash\nbru run folder\n```\n\nIf you need to use an environment, you can specify it with the `--env` option:\n\n```bash\nbru run folder --env Local\n```\n\nIf you need to collect the results of your API tests, you can specify the `--output` option:\n\n```bash\nbru run folder --output results.json\n```\n\nIf you need to run a set of requests that connect to peers with both publicly and privately signed certificates respectively, you can add private CA certificates via the `--cacert` option. By default, these certificates will be used in addition to the default truststore:\n\n```bash\nbru run folder --cacert myCustomCA.pem\n```\n\nIf you need to limit the trusted CA to a specified set when validating the request peer, provide them via `--cacert` and in addition use `--ignore-truststore` to disable the default truststore:\n\n```bash\nbru run request.bru --cacert myCustomCA.pem --ignore-truststore\n```\n\n## Importing Collections\n\nYou can import collections from other formats, such as OpenAPI, using the import command:\n\n```bash\nbru import openapi --source api.yml --output ~/Desktop/my-collection --collection-name \"My API\"\n```\n\nYou can also use the shorter form with aliases:\n\n```bash\nbru import openapi -s api.yml -o ~/Desktop/my-collection -n \"My API\"\n```\n\nThis creates a Bruno collection directory that can be opened in Bruno.\n\nYou can also import directly from a URL:\n\n```bash\nbru import openapi --source https://example.com/api-spec.json --output ~/Desktop --collection-name \"Remote API\"\n```\n\nYou can also export the collection as a JSON file:\n\n```bash\nbru import openapi --source api.yml --output-file ~/Desktop/my-collection.json --collection-name \"My API\"\n```\n\nImport Options:\n\n| Option                    | Details                                            |\n| ------------------------- | -------------------------------------------------- |\n| --source, -s              | Path to the source file or URL (required)          |\n| --output, -o              | Path to the output directory                       |\n| --output-file, -f         | Path to the output JSON file                       |\n| --collection-name, -n     | Name for the imported collection                   |\n| --insecure                | Skip SSL certificate validation when fetching from URLs |\n\n## Command Line Options\n\n| Option                       | Details                                                                       |\n| ---------------------------- | ----------------------------------------------------------------------------- |\n| -h, --help                   | Show help                                                                     |\n| --version                    | Show version number                                                           |\n| -r                           | Indicates a recursive run (default: false)                                    |\n| --cacert [string]            | CA certificate to verify peer against                                         |\n| --env [string]               | Specify environment to run with                                               |\n| --env-var [string]           | Overwrite a single environment variable, multiple usages possible             |\n| -o, --output [string]        | Path to write file results to                                                 |\n| -f, --format [string]        | Format of the file results; available formats are \"json\" (default) or \"junit\" |\n| --reporter-json [string]     | Path to generate a JSON report                                                |\n| --reporter-junit [string]    | Path to generate a JUnit report                                               |\n| --reporter-html [string]     | Path to generate an HTML report                                               |\n| --insecure                   | Allow insecure server connections                                             |\n| --tests-only                 | Only run requests that have tests                                             |\n| --bail                       | Stop execution after a failure of a request, test, or assertion               |\n| --csv-file-path              | CSV file to run the collection with                                           |\n| --reporter--skip-all-headers | Skip all headers in the report                                                |\n| --reporter-skip-headers      | Skip specific headers in the report                                           |\n| --client-cert-config         | Client certificate configuration by passing a JSON file                       |\n| --delay [number]             | Add delay to each request                                                     |\n\n## Scripting\n\nBruno cli returns the following exit status codes:\n\n- `0` -- execution successful\n- `1` -- an assertion, test, or request in the executed collection failed\n- `2` -- the specified output directory does not exist\n- `3` -- the request chain seems to loop endlessly\n- `4` -- bru was called outside of a collection root directory\n- `5` -- the specified input file does not exist\n- `6` -- the specified environment does not exist\n- `7` -- the environment override was not a string or object\n- `8` -- an environment override is malformed\n- `9` -- an invalid output format was requested\n- `255` -- another error occurred\n\n## Demo\n\n![demo](assets/images/cli-demo.png)\n\n## Support\n\nIf you encounter any issues or have any feedback or suggestions, please raise them on our [GitHub repository](https://github.com/usebruno/bruno)\n\nThank you for using Bruno CLI!\n\n## Changelog\n\n<!-- An absolute link is used here because npm treats links differently -->\n\nSee [https://github.com/usebruno/bruno/releases](https://github.com/usebruno/bruno/releases)\n\n## License\n\n[MIT](license.md)\n","packages/bruno-common/readme.md":"# bruno-common\n\nA collection of common utilities used across Bruno App, Electron and CLI packages.\n\n### Publish to Npm Registry\n\n```bash\nnpm publish --access=public\n```\n","packages/bruno-converters/readme.md":"# bruno-converters\n\nThe converters package is responsible for converting collections from one format to a Bruno collection.\nIt can be used as a standalone package or as a part of the Bruno framework.\n\n## Installation\n\n```bash\nnpm install @usebruno/converters\n```\n\n## Usage\n\n### Convert Postman collection to Bruno collection\n\n```javascript\nconst { postmanToBruno } = require('@usebruno/converters');\n\n// Convert Postman collection to Bruno collection\nconst brunoCollection = postmanToBruno(postmanCollection);\n```\n\n### Convert Postman Environment to Bruno Environment\n\n```javascript\nconst { postmanToBrunoEnvironment } = require('@usebruno/converters');\n\nconst brunoEnvironment = postmanToBrunoEnvironment(postmanEnvironment);\n```\n\n### Convert Insomnia collection to Bruno collection\n\n```javascript\nconst { insomniaToBruno } = require('@usebruno/converters');\n\nconst brunoCollection = insomniaToBruno(insomniaCollection);\n```\n\n### Convert OpenAPI specification to Bruno collection\n\n```javascript\nconst { openApiToBruno } = require('@usebruno/converters');\n\nconst brunoCollection = openApiToBruno(openApiSpecification);\n```\n\n### Convert WSDL file to Bruno collection\n\n```javascript\nimport { wsdlToBruno } from '@usebruno/converters';\n\nconst brunoCollection = await wsdlToBruno(wsdlContent);\n```\n\n## Example \n\n```javascript\n\nconst { postmanToBruno } = require('@usebruno/converters');\nconst { writeCollectionToDisk } = require('@usebruno/filestore');\nconst fs = require('fs/promises');\n\nasync function convertPostmanToBruno(inputFile, outputDir) {\n  const inputData = await fs.readFile(inputFile, 'utf8');\n\n  const collection = await postmanToBruno(JSON.parse(inputData));\n  // Writes YAML (OpenCollection) files by default\n  await writeCollectionToDisk(collection, outputDir);\n  // Pass { format: 'bru' } to write .bru files instead\n  // await writeCollectionToDisk(collection, outputDir, { format: 'bru' });\n}\n\nconvertPostmanToBruno('demo_collection.postman_collection.json', './bruno-collection');\n\n``` \n\n## WSDL Import Features\n\nThe WSDL importer supports the following features:\n\n- **Service Discovery**: Automatically extracts service endpoints from WSDL definitions\n- **Operation Mapping**: Converts WSDL operations to Bruno HTTP requests\n- **SOAP Envelope Generation**: Creates proper SOAP envelopes for each operation\n- **Header Configuration**: Sets up appropriate Content-Type and SOAPAction headers\n- **Environment Variables**: Creates environment variables for service base URLs\n- **Folder Organization**: Groups operations by port type for better organization\n\n### WSDL Import Example\n\n```javascript\nimport { wsdlToBruno } from '@usebruno/converters';\nimport { writeCollectionToDisk } from '@usebruno/filestore';\nimport fs from 'fs/promises';\n\nasync function importWSDL() {\n  const wsdlContent = await fs.readFile('service.wsdl', 'utf8');\n\n  const collection = await wsdlToBruno(wsdlContent);\n  // Writes YAML (OpenCollection) files by default\n  await writeCollectionToDisk(collection, './soap-collection');\n  // Pass { format: 'bru' } to write .bru files instead\n  // await writeCollectionToDisk(collection, './soap-collection', { format: 'bru' });\n}\n\nimportWSDL();\n```\n\n### CLI Usage\n\nYou can also use the Bruno CLI to import WSDL files:\n\n```bash\n# Import WSDL file to a directory\nbruno import wsdl --source service.wsdl --output ~/Desktop/soap-collection --collection-name \"SOAP Service\"\n\n# Import WSDL from URL\nbruno import wsdl --source https://example.com/service.wsdl --output ~/Desktop --collection-name \"Remote SOAP Service\"\n\n# Import WSDL and save as JSON file\nbruno import wsdl --source service.wsdl --output-file ~/Desktop/soap-collection.json --collection-name \"SOAP Service\"\n```\n\n## Supported Formats\n\n- **Postman Collections** (v2.1)\n- **Insomnia Collections** (v4 and v5)\n- **OpenAPI Specifications** (v3.0)\n- **WSDL Files** (Web Services Description Language)\n\n## Dependencies\n\n- `lodash` - Utility functions\n- `nanoid` - UUID generation\n- `js-yaml` - YAML parsing\n- `xml2js` - XML parsing for WSDL\n- `@usebruno/schema` - Schema validation\n","packages/bruno-converters/tests/postman/round-trip/README.md":"# Round-trip conversion tests\n\nThis directory tests conversion **fidelity** by sending a real collection through both converters and\ncomparing the result against the input:\n\n```\nPostman export ──postmanToBruno──▶ Bruno collection ──brunoToPostman──▶ Postman export\n     (input)                                                              (output)\n        └──────────────────── compared, subtree by subtree ──────────────────┘\n```\n\nCurrently only the **auth** subtree is covered ([`auth.spec.js`](./auth.spec.js)), but the harness is\nbuilt so other subtrees (body, headers, scripts, params, …) can be added as sibling files with no new\nplumbing. See [Extending](#extending) below.\n\n---\n\n## Why round-trip, and why this direction\n\nThe per-converter unit specs (`process-auth`, `request-auth`, `bruno-to-postman`, …) each exercise\n**one** converter in isolation and assert a hand-written expected object. That has two blind spots:\n\n1. **Asymmetry between the two converters** — a field renamed on import (`tokenName` → `credentialsId`)\n   but not renamed back on export is invisible to a one-sided test. A round-trip is the only thing that\n   exercises both mappings against each other.\n2. **Circular expectations** — a hand-written \"expected\" object is only as correct as the author's\n   reading of the converter. If you misread it, the test encodes the misreading.\n\nWe focus on **Postman → Bruno → Postman** (not Bruno → Postman → Bruno) because that is the direction\nwhere a **real exported `.postman_collection.json`** is the input. Real exports carry the exact key\nnames, the full v2.1 array-backed auth format, and even fields Bruno doesn't model — ground truth that\na hand-built object can't reproduce.\n\n---\n\n## Why the fixture approach\n\nTests are **data-driven off the files in [`fixtures/`](./fixtures)**. Every `*.postman_collection.json`\nis discovered automatically and run through the round-trip. This is deliberate:\n\n- **No hand-written expected values.** The input fixture's own auth is the source of truth — the test\n  asserts the output equals the input (minus documented exceptions), so adding coverage is \"drop a\n  file\", not \"write an expectation\".\n- **Real-world shape.** Fixtures are actual Postman exports, so they include the quirks (omitted\n  defaults, extra keys, ordering) that synthetic objects miss.\n\n### Extending\n\n- **More auth coverage:** export a collection of the desired auth type from Postman, drop it into\n  `fixtures/`. It's picked up automatically. A new whitelist entry is only needed if it exercises a\n  field Bruno doesn't model yet (see below).\n- **Another subtree** (e.g. body): add `body.spec.js` alongside `auth.spec.js`, reusing the same\n  fixtures. Factor the shared \"walk + diff by node path\" logic the way [`compare-auth.js`](./compare-auth.js)\n  does for auth, keeping each subtree's normalization rules in its own module.\n\n---\n\n## The whitelist and its categories\n\nComparison is **strict**: any difference in the auth subtree fails the test. Differences that are\nexpected today are recorded in `AUTH_ROUNDTRIP_WHITELIST` in [`auth.spec.js`](./auth.spec.js), each\ntagged with a `category`. The category is the whole point — it separates \"bug we haven't fixed\" from\n\"working as intended\":\n\n| Category | Meaning | Trend | Action |\n| --- | --- | --- | --- |\n| **`feature-gap`** | Bruno has no field to hold this Postman value, so it's dropped on import. | **Should shrink** as Bruno adds support. | **This is the list to focus on.** Each entry is a concrete, user-visible data loss. |\n| **`normalization`** | Intentional, semantically-lossless transformation. | **Permanent** — will never reach zero. | Leave alone. Documents *why* the round-trip isn't byte-identical. |\n\n**Where to focus: the `feature-gap` count.** It's the number of Postman auth capabilities Bruno can't\nrepresent. Driving it down is real product work; when a gap is filled, delete its whitelist entry and\nthe test stays green. The `normalization` entries are not a backlog — they're the documented contract\nof what round-tripping intentionally changes.\n\n### The two flavors of `normalization`\n\n- **Grant-scoped pruning** (oauth2): Bruno stores grant-type-specific fields, so a field unused by the\n  grant (`authUrl` on a `client_credentials` grant, `username` on anything but `password`) is dropped.\n  These entries carry a `grantType` list and are accepted **only** for grants that don't use the field —\n  so the *same* drop on a grant that *does* use it (e.g. `authUrl` on `authorization_code`) still fails.\n  That keeps the exception tight instead of blanket-allowing the field to vanish.\n- **Default materialization**: Postman omits a field and relies on a default; Bruno stores it\n  explicitly and re-emits it (apikey `in: header`, oauth1 `version`). Whitelisted only where **Bruno's\n  default equals Postman's default**, so materializing it changes nothing semantically.\n\n### Whitelist entry matching\n\nEach entry matches a diff on `(fixture, node, authType, key, kind)`:\n\n- `fixture` and `node` accept `'*'` — a field-level gap that recurs across many requests/fixtures is\n  **one wildcard entry**, not dozens of near-duplicates.\n- `grantType` (oauth2, optional) restricts an entry to specific grant types (grant-scoped pruning).\n- Matching is **value-agnostic** (`kind` + `key`, never the value) so a whitelisted field tolerates any\n  value — we're asserting \"this field is *expected* to differ\", not pinning a specific value.\n\n---\n\n## Comparison mechanics (`compare-auth.js`)\n\n### `stableStringify`\n\nAuth values are compared by a **canonical stringification** — object keys sorted, array order\npreserved — rather than by `JSON.stringify` or reference equality. Two reasons:\n\n1. **Key order is not semantic.** Postman and Bruno emit auth params in different orders; a plain\n   string compare would flag every reorder as a diff. Sorting keys makes the compare structural.\n2. **Nested values must compare by content.** The oauth2 `additionalParameters` arrays\n   (`authRequestParams` / `tokenRequestParams` / `refreshRequestParams`) are arrays of objects. We need\n   deep, order-insensitive-on-keys but order-*sensitive*-on-arrays comparison, which `stableStringify`\n   gives: object keys are sorted (order-insensitive), array element order is preserved (array order *is*\n   semantic for these params).\n\n### `normalizeAuth` — array-backed → map\n\nPostman's v2.1 auth is an array of `{key, value, type}`. We collapse it to a plain `{key: value}` map so\nthat (a) param ordering doesn't matter and (b) the `type` tag is ignored because it is not semantically meaningful. \nMissing auth normalizes to `null` so \"no auth\" compares equal on both sides.\n\n### `absent === \"\"` collapse\n\nAn absent key and an empty-string value are treated as **equal**. Postman treats a missing auth param as\nunset, and `brunoToPostman` drops empty params on export — so `state: \"\"` disappearing on round-trip is a\nlossless no-op, not a diff. Collapsing this structurally (rather than whitelisting each empty field)\nkeeps the whitelist focused on real differences and avoids churn when a fixture's empty fields change.\nConfirmed safe: Postman's importer defaults any missing auth param, so omitting empties never errors.\n\n**Exception — keys with a non-empty Postman default.** \"Absent == unset\" is false for a param Postman\nomits only because it sits at a non-empty default it still applies: oauth2 `headerPrefix` omitted means\n`Bearer`, so collapsing it would hide Bruno dropping the prefix. `POSTMAN_NON_EMPTY_DEFAULTS` in\n`compare-auth.js` materializes these on the *original* side (key absent or `null`), each with an\n`appliesWhen` predicate since the default is usually conditional — `headerPrefix` applies when the token\ngoes in the header, and an absent `addTokenTo` counts as header because Postman resolves an unspecified\nplacement to Request Headers. These predicates model **Postman's** defaults, never Bruno's importer:\nmatching the importer would make the oracle ratify it instead of checking it.\n\n### `grantType` on oauth2 diffs\n\nEach oauth2 diff is tagged with the node's grant type (from the original, falling back to the\nround-tripped side). This is what lets grant-scoped-pruning whitelist entries be conditional — the diff\ncarries enough context to say \"dropped `authUrl`, and this was a `client_credentials` grant, so it's\nfine.\"\n\n### Node-path walk\n\n`collectAuthNodes` walks the whole collection — collection-level auth, folder-level auth, and\nrequest-level auth — keying each by a stable path (`collection`, `collection/Folder`,\n`collection/Folder/Request`). Comparison is per-node by path, so a request that gains or loses auth, or\ninherits differently, is caught rather than averaged away.\n\n---\n\n## Running\n\n```bash\n# from packages/bruno-converters\nnpx jest tests/postman/round-trip/auth.spec.js\n```\n\nOn failure, each un-whitelisted diff prints a readable line — `DROPPED` (Bruno lost a field),\n`ADDED` (Bruno emitted a field Postman didn't), or `CHANGED` (value differs) — so you can tell at a\nglance whether it's a new feature gap to whitelist or a real regression to fix.\n","packages/bruno-docs/readme.md":"# bruno-docs\n\nThis is a wip.\n\nWe have a request to generate docs in a html file that can be hosted on a server so that the visitor can view the API and make requests without downloading/installing anything.","packages/bruno-electron/readme.md":"# bruno-electron\n\n```bash\n# electron dev\nnpm start\n\n# generate pfx file for signing windows build\nopenssl pkcs12 -export -inkey sectigo.key -in sectigo.pem -out sectigo.pfx\n```\n","packages/bruno-filestore/README.md":"# Bruno Filestore\n\nA generic file storage and parsing package for Bruno API client.\n\n## Purpose\n\nThis package abstracts the file format operations for Bruno, providing a clean interface for parsing and stringifying Bruno requests, collections, folders, and environments.\n\n## Features\n\n- Format-agnostic APIs for file operations\n- Currently supports Bruno's custom `.bru` format\n- Designed for future extensibility to support YAML and other formats\n\n## Usage\n\n```javascript\nconst {\n  parseRequest,\n  stringifyRequest,\n  parseCollection,\n  stringifyCollection,\n  parseEnvironment,\n  stringifyEnvironment,\n  parseDotEnv\n} = require('@usebruno/filestore');\n\n// Parse a .bru request file\nconst requestData = parseRequest(bruContent);\n\n// Stringify request data to .bru format\nconst bruContent = stringifyRequest(requestData);\n\n// Example with future format support (not yet implemented)\nconst requestData = parseRequest(yamlContent, { format: 'yaml' });\n```\n\n### Writing a converted collection to disk\n\n`writeCollectionToDisk` takes a collection as returned by `@usebruno/converters` and writes it as a collection directory that Bruno can open:\n\n```javascript\nconst { openApiToBruno } = require('@usebruno/converters');\nconst { writeCollectionToDisk } = require('@usebruno/filestore');\n\nconst collection = openApiToBruno(openApiSpecification);\nawait writeCollectionToDisk(collection, './my-collection');\n```\n\n`outputDir` is created if it does not exist. If it already exists it must be empty, otherwise the call rejects with an error whose `code` is `'ENOTEMPTY'` and nothing is written.\n\n## API\n\nThe package provides the following functions:\n\n- `parseRequest(content, options = { format: 'bru' })`: Parse request file content\n- `stringifyRequest(requestObj, options = { format: 'bru' })`: Convert request object to file content\n- `parseCollection(content, options = { format: 'bru' })`: Parse collection file content\n- `stringifyCollection(collectionObj, options = { format: 'bru' })`: Convert collection object to file content\n- `parseFolder(content, options = { format: 'bru' })`: Parse folder file content\n- `stringifyFolder(folderObj, options = { format: 'bru' })`: Convert folder object to file content\n- `parseEnvironment(content, options = { format: 'bru' })`: Parse environment file content\n- `stringifyEnvironment(envObj, options = { format: 'bru' })`: Convert environment object to file content\n- `parseDotEnv(content)`: Parse .env file content\n- `writeCollectionToDisk(collection, outputDir, options = { format: 'yml' })`: Write a whole collection to `outputDir` in `yml` or `bru` format. Resolves to `outputDir`. `outputDir` must be missing or empty; otherwise it rejects with `code: 'ENOTEMPTY'` ","packages/bruno-graphql-docs/readme.md":"# bruno-graphql-docs\n\nStandalone graphql docs explorer module forked from [graphiql](https://github.com/graphql/graphiql)\n\n### Publish to Npm Registry\n```bash\nnpm publish --access=public\n```","packages/bruno-js/readme.md":"# bruno-js\n\nProvides the script, test, vars and assert runtimes.\n\n### Publish to Npm Registry\n```bash\nnpm publish --access=public\n```","packages/bruno-lang/readme.md":"# bruno-lang\n\nThe language utils for working with `.bru` files\n\n### Publish to Npm Registry\n```bash\nnpm publish --access=public\n```","packages/bruno-query/readme.md":"# bruno-query\n\nBruno query with deep navigation, filter and map support\n\nEasy array navigation\n```js\nget(data, 'customer.orders.items.amount')\n```\nDeep navigation .. double dots\n```js\nget(data, '..items.amount')\n```\nArray indexing\n```js\nget(data, '..items[0].amount')\n```\nArray filtering [?] with corresponding filter function\n```js\nget(data, '..items[?].amount', i => i.amount > 20) \n```\nArray filtering [?] with simple object predicate, same as (i => i.id === 2 && i.amount === 20)\n```js\nget(data, '..items[?]', { id: 2, amount: 20 }) \n```\nArray mapping [?] with corresponding mapper function\n```js\nget(data, '..items..amount[?]', amt => amt + 10) \n```\n\n### Publish to Npm Registry\n```bash\nnpm publish --access=public\n```\n","packages/bruno-schema/readme.md":"# bruno-schema\n\nThe schema definition for collections\n\n### Publish to Npm Registry\n```bash\nnpm publish --access=public\n```\n\n### Collection schema\n```bash\nid                       Unique id (when persisted to a db)\nuid                      Unique id\nname                     collection name\nitems                    Items (folders and requests)\n  |-uid                  A unique id   \n  |-name                 Item name \n  |-type                 Item type  (folder, http-request, graphql-request)\n  |-request              Request object\n    |-url                Request url\n    |-method             Request method\n    |-headers            Request headers (array of key-val)\n    |-params             Request params (array of key-val)\n    |-body               Request body object  \n      |-mode             Request body mode\n      |-json             Request json body\n      |-text             Request text body\n      |-xml              Request xml body\n      |-multipartForm    Request multipartForm body\n      |-formUrlEncoded   Request formUrlEncoded body\n```\n","packages/bruno-sqlite/README.md":"# @usebruno/sqlite\n\nSQLite storage for the Bruno API client. You author migrations as `.ts` and statements as `.sql`; the package compiles them into a typed data layer for the Electron main process.\n\n## Entry point\n\n`@usebruno/sqlite` (or `/node`) — main process only. Owns the DB and runs statements, using the built-in `node:sqlite`.\n\n## Add a migration\n\n```bash\nnpm run migration:new --workspace=packages/bruno-sqlite   # prompts for a name\n```\n\nThis scaffolds a sequence-prefixed `migrations/<seq>_<name>.ts` file exporting `up` and `down`. Each returns the SQL to apply or revert the migration (a plain string also works):\n\n```ts\nexport const up = (): string => {\n  return 'CREATE TABLE users (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, email TEXT);';\n}\nexport const down = (): string => {\n  return 'DROP TABLE users;';\n}\n```\n\nMigrations apply automatically (in prefix order) when the DB opens, and their content is hashed into a `_migrations` table so an already-applied migration can't be silently edited.\n\n### Verify migrations\n\n`npm run migration:verify` replays every migration against a throwaway `VACUUM INTO` copy of a real database, so it exercises them over actual data without touching the source DB. Point it at the DB with an absolute `DB_PATH`:\n\n```bash\ncp .env.example .env      # then set DB_PATH to an absolute path\nnpm run migration:verify --workspace=packages/bruno-sqlite\n```\n\nIt fails if a migration errors, or if a migration's content no longer matches what was recorded when it was first applied to that DB.\n\n### Schema\n\n[`SCHEMA.md`](./SCHEMA.md) shows the schema the migrations produce: an ER diagram plus each table's columns, keys and indexes. The pre-commit hook regenerates it whenever a migration is staged; `npm run schema` regenerates it by hand.\n\n## Add statements (sqlc syntax)\n\nWrite them in `statements/*.sql` — multiple per file. Each starts with a `-- name:` annotation, and params are named (`@param`):\n\n```sql\n-- name: list_users :many\nSELECT id, name, email FROM users ORDER BY name;\n\n-- name: get_user :one\nSELECT id, name, email FROM users WHERE id = @id;\n\n-- name: create_user :exec\nINSERT INTO users (name, email) VALUES (@name, @email);\n```\n\n| annotation | runs as | returns |\n|---|---|---|\n| `:one` | single row | row or `undefined` |\n| `:many` | rows | array |\n| `:exec` | write | `{ changes, lastInsertRowid }` |\n\nRun `npm run generate` (or `build`) to compile statements into the typed registry.\n\n## Use it — main process\n\n```js\nconst { createDatabase } = require('@usebruno/sqlite');\n\nconst { db, statements } = createDatabase('/path/to/bruno.db');\n\nstatements.execute('create_user', { name: 'Ada', email: 'ada@example.com' });\nconst users = statements.execute('list_users');       // no params\nconst ada   = statements.execute('get_user', { id: 1 });\n// db.close() on shutdown\n```\n\nParams are always an **object** keyed by the named parameters (no positional/array binding).\n\nThe package does not expose statements to a renderer. The host application registers its own IPC handler for each statement it wants to expose.\n\n## Files\n\nRows are a poor home for a large payload, so the package ships a `files` table with a store in front\nof it. Each entry is kept in whichever shape fits its size:\n\n| size | stored as |\n|---|---|\n| up to 1MB | a `BLOB` on the row |\n| over 1MB | a file next to the database, with the row pointing at it |\n\n`createDatabase` returns the store alongside the statements:\n\n```js\nconst { db, statements, files } = createDatabase('/path/to/bruno.db');\n\nconst entry = await files.write(buffer, { contentType: 'image/png' });  // { id, size, inline, contentType }\nconst bytes = await files.read(entry.id);                                // Uint8Array, or null\nfiles.stat(entry.id);\nawait files.remove(entry.id);\n```\n\n`read` returns the whole payload, from the row or from the spilled file.\n\nThe files directory defaults to `<database path minus extension>-files`; pass `filesDir` to place it\nelsewhere, and `inlineMaxBytes` to move the threshold.\n\n### Lifetime\n\nA file lives for as long as some row points at it. Declare that with a foreign key:\n\n```sql\nALTER TABLE runner_responses ADD COLUMN body_file_id INTEGER REFERENCES files(id);\n```\n\n`files.collect()` reads the foreign keys back off the schema, deletes every row no table references,\nremoves the files they own, and sweeps files on disk that no row names. Nothing has to be registered\nwith the store — adding the column is enough. Call it at startup.\n\nAs a safety rule, when *no* table references `files` the store leaves rows alone: with no referrers\nevery row would look unreferenced. The disk sweep still runs.\n\n### Reading files from the renderer\n\nThe package exposes no renderer API for files, just as for statements. Writes are main-process only; to let the renderer read a file, the host registers its own IPC handler for that read.\n\n## Development\n\n```bash\nnpm run build --workspace=packages/bruno-sqlite    # generate + bundle + types\nnpm run watch --workspace=packages/bruno-sqlite    # rebuild on change\nnpm run test  --workspace=packages/bruno-sqlite    # jest\n```\n\n`.sql` files are the only source of truth committed; `src/generated/` and `dist/` are git-ignored and produced by the build.\n","packages/bruno-tests/collection/readme.md":"# bruno-tests collection\n\nAPI Collection to run sanity tests on Bruno CLI.\n","packages/bruno-tests/collection_level_oauth2/readme.md":"# bruno-tests collection\n\nAPI Collection to run sanity tests on Bruno CLI.\n","packages/bruno-tests/collection_oauth2/readme.md":"# bruno-tests collection\n\nAPI Collection to run sanity tests on Bruno CLI.\n","packages/bruno-tests/readme.md":"# bruno-tests\n\nThis package is used to test the Bruno CLI.\nWe have a collection that sits in the `collection` directory.\n\n### Test Server\n\nThis will start the server on port 80 which exposes endpoints that the collection will hit.\n\n```bash\n# install node dependencies\nnpm install\n\n# start server\nnpm start\n```\n\n### Run Bru CLI on Collection\n\n```bash\ncd collection\n\n# run collection against local server\nnode ../../bruno-cli/bin/bru.js run --env Local --output junit.xml --format junit\n\n# run collection against prod server hosted at https://testbench.usebruno.com\nnode ../../bruno-cli/bin/bru.js run --env Prod --output junit.xml --format junit\n```\n\n### License\n\n[MIT](LICENSE)\n","packages/bruno-tests/reports/README.md":"# Bruno CLI — Docker run reports\n\nThis folder is the bind-mount target for the Docker Compose example at [`../docker-compose.yml`](../docker-compose.yml).\n\nWhen you run:\n\n```bash\ncd packages/bruno-tests\ndocker compose run bruno-cli\n```\n\nthe container writes three report files into this directory:\n\n| File | Format | Purpose |\n|------|--------|---------|\n| `results.json` | JSON | Machine-readable run summary (request/response, timings, assertion results) |\n| `results.xml` | JUnit XML | For CI test reporters (GitHub Actions, GitLab CI, Jenkins, etc.) |\n| `results.html` | HTML | Human-readable report you can open in a browser |\n\nThe files themselves are gitignored — only this `README.md` and the `.gitignore` are tracked, which keeps the folder present in the repo so the `./reports:/reports` bind mount has somewhere to land without needing manual `mkdir` first.\n\nTo skip a format, drop the corresponding `--reporter-*` flag from the `command:` block in `docker-compose.yml`.\n"},"files":{".claude/CLAUDE.md":"# Project Guide\n\nBruno fork — open-source API client (Electron + React + Redux).\n\nBruno is an API **client**, not a server: it sends requests and handles responses (auth, params, headers, bodies, environments, cookies). Judge every behavior and edge case by \"what should a *client* do here?\" — surface malformed or hostile responses clearly and stay robust; don't enforce server-side correctness.\n\n## Quick Commands\n\n```bash\nnpm run dev              # Run electron + react concurrently\nnpm run dev:watch        # Same, with hot-reload of the electron main process\nnpm run storybook        # Component dev (bruno-app)\nnpm run lint:fix         # ESLint fix\nnpm test --workspaces    # Unit tests (Jest) across all packages\n```\n\nFull script list: root `package.json`. Setup: `nvm use && npm i --legacy-peer-deps && npm run setup` (Node **v22.12.0**, see `.nvmrc`).\n\n### Running a single test\n\n```bash\n# Single unit test (Jest) — scope to the workspace, pass a path/pattern after --\nnpm test --workspace=packages/bruno-app -- path/to/file.spec.js\nnpm test --workspace=packages/bruno-requests -- -t \"test name pattern\"\n\n# Single e2e test (Playwright) — a project flag is REQUIRED (config has no default)\nnpx playwright test tests/collection/create-collection.spec.ts --project=default\nnpx playwright test --project=default -g \"test name pattern\"\n```\n\nPrefer the smallest scope — one workspace, one spec — over `--workspaces` or the full e2e run.\n\n### Building shared packages\n\n`npm run dev` does **not** rebuild the shared packages (bruno-common, bruno-requests,\nbruno-filestore, bruno-sqlite, bruno-converters, bruno-query, bruno-schema-types, bruno-graphql-docs).\nAfter editing one, rebuild it or run its watcher, or the app won't pick up changes:\n\n```bash\nnpm run build:bruno-common      # also :bruno-requests, :bruno-filestore, :bruno-sqlite, etc.\nnpm run watch:common            # also watch:requests, watch:converters\n```\n\n## Key Architecture\n\n- **Main process entry**: `packages/bruno-electron/src/index.js`\n- **Renderer + Redux store**: `packages/bruno-app/src/providers/ReduxStore/`\n- Packages live under `packages/`, with top-level `tests/` and `playwright/`.\n\n## Testing\n\n- **Unit**: Jest, config per-package (`packages/*/jest.config.js`); bruno-app uses jsdom + `jest.setup.js`.\n- **E2E**: Playwright — fixtures, helpers, isolation rules, and pitfalls live in\n  `.claude/rules/testing.md`; use the `write-e2e-test` skill when adding specs.\n\n## Coding Standards\n\nFull list: `CODING_STANDARDS.md`. Mechanical style (indent, quotes, semicolons, no trailing\ncommas, arrow-param parens) is ESLint-enforced — run `npm run lint:fix`, don't hand-police it.\nNon-obvious project rules worth holding:\n\n- React: avoid `useEffect` (prefer derived state / custom hooks); import hooks by name, never `React.useX`; a component is controlled XOR uncontrolled; Tailwind for layout, styled-components `theme` for colors.\n- Every component owns a directory — `<ComponentName>/index.js(x)`, never `<ComponentName>.js(x)`.\n- Extract for readability or clear reuse; avoid only indirection that adds no payoff. Add `data-testid` attributes for Playwright selectors.\n\n## Detailed rules & references\n\nPath-scoped rules in `.claude/rules/` auto-attach when you touch matching files:\n`architecture.md` (`@usebruno/*` dependency boundaries, manifests), `bruno-app-layout.md` (bruno-app\nmodule layout), `electron-ipc.md` (IPC handlers + startup), `redux-store.md` (slices/middleware),\n`sqlite.md` (`@usebruno/sqlite` — migrations, statements, per-statement IPC), `testing.md` (e2e\npatterns & gotchas), `cross-platform.md` (Windows file/process/path pitfalls), `dsl-changes.md`\n(on-disk `.bru`/`.yml` format & backward compat), `feature-parity.md` (co-related features —\napp↔CLI twins, converter/shim/level/protocol pairs), `conventions.md` (readability, reuse,\nreplacement & pre-submit hygiene).\n\nRead on demand (not auto-loaded): `.claude/reference/architecture.md` — the monorepo map, request\npipeline, sandbox, core types, and dependency versions. Consult it before cross-package or\narchitectural work.\n",".claude/rules/architecture.md":"---\npaths:\n  - \"packages/**/*\"\n---\n\n# Dependency Boundaries\n\nInternal `@usebruno/*` dependencies (from each `package.json`) form a strict DAG. **New code must\nrespect it** — a cyclic or upward dependency is an architectural bug, not a convenience.\n\nThe full monorepo map (build tools, request pipeline, sandbox, file formats, core data-model\ntypes, dependency versions) is the on-demand reference `.claude/reference/architecture.md` — read\nit before non-trivial cross-package or architectural work.\n\n## Dependency direction & ownership boundaries\n\n- **Leaf libs — zero internal `@usebruno/*` deps:** bruno-common, bruno-lang, bruno-query,\n  bruno-requests, bruno-graphql-docs, bruno-schema, bruno-schema-types, bruno-toml, bruno-sqlite\n  (a leaf, but *not* platform-neutral — see guardrail 6).\n- **Mid consumers:** bruno-js → (common, query); bruno-converters → (common, schema; schema-types\n  as devDep); bruno-filestore → (common, lang; schema-types as devDep).\n- **Top consumers (things flow *into* them, never out):** bruno-cli → (common, converters,\n  filestore, js, lang, requests); bruno-electron → (common, converters, filestore, js, lang,\n  requests, schema, sqlite); bruno-app → (common, converters, graphql-docs, schema).\n\nGuardrails this enforces:\n\n1. **bruno-common is the browser-safe base leaf.** It runs in the web renderer (`bruno-app`), not\n   just Node, so it must stay platform-neutral: no Node built-ins (`fs`, `path`, `os`, `crypto`,\n   `child_process`, `node:*`) and no dependency that itself pulls in Node. It currently ships with\n   zero runtime dependencies — keep it that way. It also depends on no other `@usebruno/*` package;\n   a util that needs Node or another bruno package belongs in a different package.\n2. **No shared/library package may import bruno-app or bruno-electron.** Renderer- or\n   Electron-specific code must not be pushed down into a lib to make it importable upward.\n3. **bruno-js must stay Electron-free.** It runs in both the Electron main process *and* the CLI\n   (bruno-cli → js). Adding an `electron`/IPC import to bruno-js breaks the CLI. Sandbox logic\n   belongs in bruno-js; host wiring belongs in bruno-electron.\n4. **bruno-schema-types is types-only** (a *devDependency* of converters/filestore, built with\n   plain `tsc`). Import types from it; never add runtime code or a runtime import of it.\n5. **bruno-schema (Yup) and bruno-schema-types (TS types) are distinct and both live.** app +\n   converters use `@usebruno/schema` for runtime validation; filestore + converters use\n   `@usebruno/schema-types` for compile-time types. A data-model change usually touches both.\n6. **bruno-sqlite is main-process only.** `@usebruno/sqlite` (= `/node`) imports `node:sqlite`,\n   `node:fs` and `node:crypto`; importing it from the renderer breaks the bundle. The renderer\n   reaches statements only through the per-statement IPC handlers in bruno-electron. See\n   `.claude/rules/sqlite.md`.\n\n## Declared dependencies must match real imports\n\nA package's `package.json` is the contract; workspace hoisting hides breaches of it. Declare every\nimport in the manifest of the package that imports it — one resolving only because a sibling or the\nroot happens to pull it in is a latent break. Test- and build-only packages belong in\n`devDependencies`; anything in `dependencies` ships to users with its transitive tree. Drop a\ndeclaration when the change removes its last consumer.\n",".claude/rules/bruno-app-layout.md":"---\npaths:\n  - \"packages/bruno-app/**/*\"\n---\n\n# Module & Directory Layout (bruno-app)\n\nLayout is predictable enough here to be load-bearing, so a misplaced file is an architectural defect,\nnot a style nit — a breach blocks, the same as a dependency-direction violation in\n`.claude/rules/architecture.md`.\n\n- **A component owns a directory: `<ComponentName>/index.js(x)`, never `<ComponentName>.js`** — at\n  every nesting depth, including a sub-component of a component. Same shape for hooks\n  (`src/hooks/useX/index.js`) and `src/ui/` primitives.\n- **Everything a component owns lives in its directory** beside its `index.js` — `StyledWrapper.js`,\n  spec, component-specific utils. Reaching up into a parent's `StyledWrapper.js`, or leaving a\n  component's styles outside its directory, hands ownership of them to no one.\n- **`src/ui/` is a leaf**: app-agnostic presentational primitives only, and it must not import from\n  `src/components/` — the edge runs `components → ui`, never back. A `ui/` module needing one either\n  isn't a primitive, or its dependency belongs in `ui/` too.\n- **`utils/` is for stateless helpers you call.** A module *registered* into something — an editor\n  extension, a plugin, a node view, a keymap — is behavior or configuration, and filing it under\n  `utils/` hides that it's part of a schema or document contract. Give those their own directory.\n\nExisting breaches — bare `PascalCase.js(x)` component files, and `ui/MenuDropdown`'s import of\n`components/Dropdown` — are drift, not a second pattern. Don't extend them, and don't raise an\nuntouched one as a finding.\n",".claude/rules/conventions.md":"---\npaths:\n  - \"packages/**/*\"\n  - \"tests/**/*\"\n  - \"scripts/**/*\"\n---\n\n# Coding Conventions, Readability & Hygiene\n\n`CODING_STANDARDS.md` is the source of truth for Bruno's coding standards — read it. This file is\nthe judgment layer over those standards: the readability and code-hygiene calls a linter can't make.\nCode and comments must read as a natural, permanent part of the project — never as artifacts of the\ntask or session that produced them.\n\n## Style & formatting\n\nThe mechanical style — 2-space indent, single quotes (double for JSX/TSX attributes), semicolons,\nno trailing commas, parenthesized arrow params, spacing around arrows, no space before call parens\n— is enforced by ESLint and auto-fixed by `npm run lint:fix`. These deviations are real but\nlow-value to catch by hand: note them briefly rather than dwelling. Naming and casing that ESLint\ncan't mechanically repair still warrant attention.\n\n## Readability\n\n- **Descriptive names.** Functions and variables carry concise, descriptive names; an unclear or\n  misleading name is worth raising even when the code is otherwise correct.\n- **Reuse before you write.** Before adding a component, hook, or helper, search for the one that\n  already exists — by concept, not by the name you'd have picked — and read the nearest sibling\n  solving the same shape of problem, since its call site shows the intended composition. Reuse is\n  usually a *net deletion* — the bespoke markup and its CSS go. Where the existing primitive is\n  *almost* right, widen it rather than standing up a near-duplicate beside it; two near-identical\n  implementations diverge silently.\n- **Extraction & abstraction.** Extract a helper or shared abstraction whenever it genuinely\n  improves readability or serves a clear, anticipated reuse — this is encouraged and not gated on a\n  minimum number of call sites; suggest it where it would help. Avoid only *unnecessary* abstraction:\n  a layer that adds indirection without improving clarity or earning reuse — a generalized utility\n  built for a single site with no foreseeable second user, or options/config added \"for later.\"\n  Breaking a long, complex function into well-named local helpers for readability is always fine.\n- **Single-line indirection.** A one-line function that only forwards to another — adding a stack\n  frame without adding meaning — should be inlined.\n- **Optional chaining.** `?.` belongs only where the null case is handled right there (a fallback,\n  early return, or guard). Used elsewhere it hides whether a value can genuinely be null and works\n  against TypeScript's guarantees; fix the type or narrow first.\n- **Comments explain the why.** Genuinely complex flow deserves a comment covering the rationale an\n  obvious reading can't; self-explanatory code does not. Full bar in **Comments** below.\n- **Functional, but readable.** Prefer obvious, linear pipelines over deep functional machinery\n  (ADTs, monads) — the code should stay easy for any contributor to follow and extend.\n\n## Comments\n\n- **No situational or prompt-driven comments.** A comment must not reference the change, the task, or\n  the moment it was written. Drop `// added to fix ...`, `// as requested`, `// new logic for X`,\n  `// updated to handle ...`, `// per review`. If the reason genuinely matters, state it as a\n  timeless fact about the code (or link the issue/PR) — not as \"what I just did\".\n- **No obvious comments.** Don't restate what the code already says. `// loop over items` above a\n  loop, `// set the name` above `obj.name = name`, `// return the result` — these add nothing. If the\n  code is self-explanatory, leave it uncommented.\n- **Comment the why, not the what.** Reserve comments for what the code can't show: non-obvious\n  rationale, invariants, edge cases, a workaround and the constraint forcing it, units, or a pointer\n  to a spec/issue. These stay useful long after the change lands.\n- **No scaffolding or narration.** No `// ... existing code ...`, no placeholder or TODO-for-me\n  notes, no changelog or step-by-step narration in comments, no commented-out code left behind.\n\n## Beyond comments\n\n- **Anything added needs a live consumer in the same change.** No options, parameters, or\n  configuration \"for later\"; no payload field no reader destructures, branch for a state the producer\n  can't emit, or ignored parameter. Each is dead on arrival and reads as a contract honored somewhere\n  else — if the consumer is a follow-up, leave it out. Extracting a helper for readability or clear,\n  foreseeable reuse is fine (see **Readability → Extraction & abstraction** above).\n- Match the surrounding code's style and naming so a change is indistinguishable from the existing\n  codebase, not visibly bolted on.\n- Keep diffs minimal — no unrelated reformatting or whitespace churn. A drive-by cleanup worth doing\n  belongs in its own commit, and never in a *different package* than the change is about; interleaved,\n  reviewers can't separate scope creep from load-bearing edits.\n\n## Replacing code leaves nothing behind\n\nHalf a migration reads as a complete one — the leftovers survive review and mislead whoever edits the\nfile next. Closing the loop is part of the change.\n\n- **Follow what you replaced to every reference and remove it there** — the CSS whose selectors the\n  new markup no longer emits, the import whose last use just went, the prop nothing passes. Styles are\n  the easiest to leave behind and the most misleading when left: CSS for a class that renders nothing\n  fails silently and looks intentional.\n- **Don't copy a block to its new home and leave the original.** Where both copies must genuinely\n  exist — separate call sites, or one rule enforced on both sides of a process boundary — name the\n  invariant in a comment at both, because nothing else keeps them in sync.\n- **Reconcile the whole flow, not the entry point.** Reshaping something that crosses a boundary — a\n  payload, a serialized field, a lookup key — means walking to every consumer on the far side.\n  Deleting a control (a sanitizer, a validation or escaping step) is a decision to state, not a side\n  effect of a rewrite.\n\n## Before you call it done\n\n- **Run the affected workspace's tests**, not just the specs you wrote.\n- **Changed a return value or payload shape?** Other specs assert it too — exact-equality assertions\n  elsewhere break on an added key.\n- **Walk the acceptance criteria against the diff one at a time.** Compound criteria are what slip:\n  the half of the sentence you didn't have open never got exercised.\n",".claude/rules/cross-platform.md":"---\npaths:\n  - \"scripts/**/*\"\n  - \"packages/bruno-electron/**/*\"\n---\n\n# Cross-Platform Guidelines\n\nThis is an Electron app targeting macOS, Windows, and Linux. All code touching the filesystem, child processes, signals, or paths must work on all three platforms.\n\n## Filesystem\n\n- `fs.rmSync` with `force: true` only suppresses `ENOENT`, NOT `EPERM`/`EBUSY`. Windows locks files aggressively (antivirus, indexing, open handles). Always use `maxRetries` and `retryDelay` when deleting directories.\n- Use `path.join()` / `path.resolve()` for all paths — never hardcode `/` separators.\n- Windows paths are case-insensitive. Use `normalizePath()` (from `utils/common/path`) when comparing collection/workspace paths.\n- `app.getPath()` returns platform-specific directories (`userData`, `documents`, etc.). Never assume POSIX-style locations.\n- File watchers (chokidar) may emit different event orderings across platforms. Don't depend on specific add/change/unlink sequences.\n\n## Child Processes\n\n- `spawn('npm', [...])` requires `shell: true` on Windows because `npm` is `npm.cmd`.\n- `child.kill()` with `shell: true` only kills the shell wrapper (`cmd.exe`) on Windows, leaving orphaned children. Use `taskkill /pid <pid> /T /F` to kill the entire process tree on Windows.\n- `execSync` / `spawn` commands must avoid Unix-only syntax (`&&` chains work in cmd.exe, but pipes and redirects differ).\n\n## Signals & Shutdown\n\n- `SIGINT` / `SIGTERM` are unreliable on Windows with `shell: true`. Also handle `SIGHUP` as fallback.\n- App shutdown must close all file watchers. Each watcher class implements `closeAllWatchers()`, orchestrated by `closeAllWatchers()` in `index.js`.\n\n## Line Endings\n\n- Files authored on Windows use CRLF. When parsing multiline `.bru`/text blocks line by line, split with a CRLF-aware regex (`/\\r\\n|\\r|\\n/`), never on `\\n` alone — a trailing `\\r` otherwise leaks into parsed values and causes spurious dirty-state and diffs. the `.split(/\\r\\n|\\r|\\n/)` in `bruno-lang`'s `v2/src/envToJson.js` is the reference pattern; keep new parsers consistent with it.\n\n## stdout vs stderr\n\n- Dev tools (rsbuild, webpack, electron-builder) may route startup output to stderr on Windows. When detecting patterns in process output, check both streams.\n\n## Platform-Specific Dependencies\n\n- `forceInstallPlatformDeps()` in `scripts/setup.js` installs platform-specific native modules (e.g., `@lydell/node-pty-{platform}-{arch}`).\n- Electron builder config handles platform-specific packaging (`mac`/`win`/`linux` targets).\n\n## Path Separators in Collection/Workspace Stores\n\n- electron-store persists paths as-is. A workspace opened on macOS stores `/Users/...`, on Windows `C:\\Users\\...`.\n- `ELECTRON_USER_DATA_PATH` only applies when `isDev` is true (`index.js:22`).\n",".claude/rules/dsl-changes.md":"---\npaths:\n  - \"packages/bruno-app/**\"\n  - \"packages/bruno-electron/**\"\n  - \"packages/bruno-cli/**\"\n  - \"packages/bruno-lang/**\"\n  - \"packages/bruno-filestore/**\"\n  - \"packages/bruno-schema/**\"\n  - \"packages/bruno-schema-types/**\"\n  - \"packages/bruno-converters/**\"\n---\n\n# On-Disk DSL & Serialization Changes\n\nBruno persists everything users create — collections, requests, environments, folders, and\nconfig — as plain-text files on the user's disk and in their Git repos. Two on-disk formats\nexist:\n\n- **`.bru`** — the Bru DSL. Read/write goes through the **v2 (ohm-js)** grammar in\n  `packages/bruno-lang/v2/src`, via `packages/bruno-filestore/src/formats/bru`\n  (`bruToJsonV2` / `jsonToBruV2`). Make all `.bru` changes in v2.\n- **`.yml`** — OpenCollection YAML, handled by `packages/bruno-filestore/src/formats/yml`. This is\n  the current `DEFAULT_COLLECTION_FORMAT` (canonical: `packages/bruno-filestore/src/constants.ts`,\n  duplicated for the renderer in `packages/bruno-app/src/utils/common/constants.js`), so new\n  collections are `.yml` unless the user chooses otherwise.\n- Plus `bruno.json` (per-collection config, passed through `stringifyCollection`) and `.env` files\n  (`parseDotEnv` / `dotenvToJson`).\n\n(`packages/bruno-toml` exists but is **orphaned** — imported nowhere outside its own test and on no\nserialization path; a DSL change never needs it. `preferences.json` is app-level electron-store\nstate under `userData`, not part of a collection's on-disk contract, so it is out of scope here.)\n\nThese files outlive the version that wrote them: an older Bruno reads files written by a\nnewer one, teammates on different versions share the same repo, and users hand-commit these\nfiles. **A change to the on-disk shape is a change to a public contract.** Get it wrong and\nyou break parsing, corrupt collections, or silently drop user data — on collections you\ncan't reach to fix.\n\nThe in-memory objects that become these files are often assembled far from the serialization\nlayer — in `bruno-electron` (IPC handlers) or `bruno-app` (Redux) — and handed to\n`bruno-filestore`, which can't tell that a field's shape changed upstream. So a DSL-affecting\nchange can originate in *any* package on the data path, not just the format packages. Treat\nthis rule as in scope for changes anywhere the collection/request/environment/config object\nis built, mutated, or serialized — the serialization packages plus `bruno-app`,\n`bruno-electron`, and `bruno-cli`, where these objects are assembled or read/written. Adding a\npersisted field in app/electron without wiring it through `bruno-filestore` (both formats) and\n`bruno-schema` silently drops or rejects data on save — the exact bug this rule guards against.\n\n## Rules\n\n**Avoid unnecessary DSL changes.** First ask whether the need can be met in memory, in the\nUI, or as a derived value without touching the serialized shape at all — most enhancements\ncan. Only change the on-disk format when the data genuinely must persist.\n\n**Additive and optional only.** A new field must be optional with a safe default, so files\nwithout it still parse and behave. Never rename, remove, repurpose, or retype an existing\nfield, and never change its semantics — old files and old app versions depend on the\ncurrent meaning.\n\n**Round-trip must be lossless.** `parse(stringify(x))` must equal `x`, and `stringify` must\nnever drop fields it doesn't recognize. A file written by a newer version, opened and\nre-saved by an older one, must not lose the newer fields.\n\n**Prove escaping on the written bytes, not in memory.** When a value is embedded in a\ndelimited block (`.bru` multiline `'''…'''`, annotation args), an in-memory `escape`/`unescape`\nround-trip can pass while the on-disk file still corrupts — the parser terminates at the first\nre-introduced delimiter. Test `parse(read(serialize(x)))` on the actual bytes, and fuzz across\nquote/backslash runs. Escape at single-character granularity: escaping a multi-character\ndelimiter by splitting on the whole delimiter (`split(\"'''\")`) leaves fragments that recombine\non runs of length ≥ the delimiter; escape the backslash first, then every single delimiter char.\nRef: `escapeMultilineDescription` in `bruno-lang/v2/src/utils.js`.\n\n**New syntax breaks older parsers — not just older fields.** The additive-and-optional rule\nprotects field-level compat, but adding new `.bru` *syntax* (a new escape form, block delimiter,\nor annotation grammar) makes files written by the new version fail to *parse* in older versions,\nwhich have no handling for it. This is a forward-compat break beyond field loss; it needs an\nexplicit decision and a compatibility path before shipping.\n\n**Descriptions are string-only internally.** Every inline `description` field (headers, params,\nassertions, variables, body entries, ...) is stored as a plain `string`. On the **yml** side,\nparsers accept a bare string *or* a legacy `{ content }` object on read and normalize to the\nstring (`formats/yml/common/{headers,variables,assertions,actions}.ts`, `parseEnvironment.ts`,\n`body.ts`); writers emit a bare string. This read-accepts-object / write-emits-string asymmetry is\nan established convention — not a lossy round-trip — proven by `formats/yml/common/variables.spec.ts`.\nThe `.bru` side has no `{ content }` handling; it stores descriptions as annotation/textblock\nstrings straight from the grammar. New inline description-like fields should follow this\nstring-only shape.\n\n*Exception — `docs` is not string-only.* Collection/folder documentation (`docs`) is written by\nthe **yml** layer as an **object** `{ content, type: 'text/markdown' }`\n(`formats/yml/stringifyCollection.ts`, `stringifyFolder.ts`) and normalized back to a string on\nread; the `.bru` side writes it as a plain textblock. So \"follow the string-only shape\" applies to\ninline `description`, *not* to `docs`-style documentation fields — check which family a new field\nbelongs to before copying either pattern.\n\n**Keep both formats in lockstep.** Any field added or changed must be handled in *both*\n`bru` and `yml` — parse and stringify on each side — or the same collection behaves\ndifferently depending on its format. There is **no dedicated `bru`↔`yml` converter**: a request\nmoves between formats by being parsed in its source format to the shared in-memory object and\nre-stringified in the target format (see `SaveTransientRequest` passing `sourceFormat` +\n`targetFormat` to `renderer:save-transient-request`). So a field handled in only one format is\n**silently dropped** the moment a request is copied/saved from a `.bru` collection into a `.yml`\none (or vice-versa). Pay attention to the *unset* case: a field that defaults or persists one way\nin `.bru` and another in `.yml` (e.g. one path returns `''`, the other `'1'`) is a divergence bug.\nThe default app-data workspace and custom-filesystem workspaces are a second such twin — a value\nthat persists in one but is silently dropped in the other is the same class of bug. Update every\nlayer the field touches: `bruno-schema-types` (types), `bruno-schema` (Yup validation),\n`bruno-filestore` (both formats), `bruno-converters` (import/export), and the grammar in\n`bruno-lang` **v2** if `.bru` syntax itself changes.\n\n**`bruno-schema` (Yup) is not optional — it will reject your field.** The collection/request/\nenvironment Yup schemas are declared `.noUnknown(true).strict()`, and they are validated on the\n**save path** (e.g. `itemSchema.validate` / `environmentSchema.validateSync` in\n`bruno-app`'s `slices/collections/actions.js` and `bruno-electron`'s `store/global-environments.js`).\nA new serialized field that round-trips through filestore but is **not** added to\n`packages/bruno-schema` will throw a validation error on save. `bruno-schema` (runtime Yup) and\n`bruno-schema-types` (TS types) are distinct and both must be updated.\n\n**When a shape must change, migrate on read — never make users edit files.** Add a\nread-time compatibility shim that upgrades the old shape as it is parsed, following the\nexisting patterns: `ensureAuthV3Rc1BackwardsCompatibility` in `formats/yml/parseItem.ts`,\nand the pre-v3 status/statusText swap in `formats/bru/index.ts`. Gate any eventual removal\nbehind a major version bump and leave a dated `TODO(remove after vN)`.\n\n**Design for scale and consistency.** New keys follow the naming and nesting of the existing\n`meta`/section blocks (`meta.name`, `meta.seq`, `meta.type`, `meta.tags`, ...). Prefer\nstructures that extend cleanly — a keyed list over a positional one, an object over a\nwidening union — and match how neighbouring fields are already modelled. Note the on-disk\n`meta {}` block does **not** map 1:1 to the type: in `bruno-schema-types` those keys are flattened\nonto `Item` (`seq`, `name`, `type`, `tags`, `description` directly on the item), and the v2 ohm\ngrammar treats `meta` as an open dictionary — the four names above are the *modeled* ones, not a\nclosed set.\n\n**Name properties like they're permanent — because they are.** A DSL key is written once and\neffectively forever: renaming it breaks every existing file and forces a compat shim. These\nnames are also user-facing — people read and hand-edit `.bru`/`.yml` — and are read by AI\nagents reasoning over collections. Choose clear, descriptive, fully spelled-out names, with\nno cryptic abbreviations or ambiguity, consistent with existing keys, and get it right the\nfirst time. A property name deserves stricter scrutiny than an ordinary variable.\n\n**Test the contract.** Add round-trip tests (parse → stringify → parse) for the new field in\n*both* formats, plus a golden fixture of the *old* format that must still parse unchanged. The\npaired `parseItem.spec.ts` / `stringifyItem.spec.ts` (and `parse*`/`stringify*` for collection,\nfolder, environment) next to each serializer in `bruno-filestore/src/formats/yml` are the pattern;\n`common/variables.spec.ts` and `common/datatype.spec.ts` show field-level cases.\n\n## A new persisted field — the files it usually touches\n\nAdding a single field tends to span several packages (a `.bru`-centric change lands mostly in\n`bruno-lang/v2`):\n\n- `bruno-lang/v2/src` — the `.bru` entry points: `bruToJson.js`, `collectionBruToJson.js`,\n  `envToJson.js`, `jsonToBru.js`, `jsonToCollectionBru.js`, `jsonToEnv.js`, plus `utils.js` for\n  shared helpers.\n- `bruno-filestore/src/formats/yml` — parse **and** stringify for the `.yml` side.\n- `bruno-schema/src/collections/index.js` — the Yup schema (skip this and it fails on save).\n- `bruno-schema-types/src/collection/item.ts` — the TypeScript type.\n- `bruno-converters` — a **separate field list from `bruno-filestore`**, not the same code path:\n  `src/opencollection/items/{http,graphql,grpc,websocket}.ts` maps the field for the OpenCollection schema, might have typescript issues to be consulted with https://github.com/opencollection-dev/opencollection when needed\n  export *and* import, and `src/postman/bruno-to-postman.js` for Postman export. A field wired\n  through filestore but not here round-trips on disk and is silently lost on export.\n- `bruno-app/src/utils/collections/index.js` — `transformRequestToSaveToFilesystem` and siblings\n  whitelist fields before save; a field missing here never reaches the serializer.\n- `bruno-electron/src/ipc/yml-migration.js` — BRU → YML migration (bru parse → yml stringify, plus\n  `bruno.json` → `opencollection.yml` config transforms); verify the field survives migration.\n- the collection utils in `bruno-electron` / `bruno-app` that build the object.\n- Round-trip specs next to the serializers (`formats/yml/parseItem.spec.ts` + `stringifyItem.spec.ts`).\n\nFind a comparable field already in the code and follow how it's wired before adding yours.\n\n## Before changing the DSL — checklist\n\n- [ ] Change is genuinely necessary (can't be solved in memory / UI)\n- [ ] New field is optional with a safe default; nothing renamed, removed, or retyped\n- [ ] Handled in both `bru` and `yml`, parse **and** stringify\n- [ ] Types (`bruno-schema-types`), Yup schema (`bruno-schema`), and converters updated —\n      \"converters\" means `bruno-converters/src/opencollection/items/*` in **both** directions,\n      which is a different field list from `bruno-filestore`\n- [ ] Field survives the app save transform and BRU → YML migration (see `feature-parity.md` §2)\n- [ ] Old files still parse — read-time compat shim added if the shape changed\n- [ ] No new `.bru` syntax that older parsers can't read (or a forward-compat path decided)\n- [ ] Any new escaping proven via on-disk reparse + fuzz, escaping single chars not the delimiter\n- [ ] `bru`/`yml` (and default vs custom workspace) behave identically, including the unset case\n- [ ] Round-trip + old-format fixture tests added\n- [ ] Naming/structure consistent with existing blocks and scalable\n",".claude/rules/electron-ipc.md":"---\npaths:\n  - \"packages/bruno-electron/**/*\"\n---\n\n# Electron Main Process Guide\n\nThe main process (`packages/bruno-electron/src/index.js`) owns the filesystem, network, child\nprocesses, and all electron-store data. The renderer reaches it **only** over IPC through the\npreload bridge — there is no direct Node access in the renderer despite `nodeIntegration: true`\n(see Security below). Before adding or changing any IPC, read this file plus the target\n`src/ipc/<domain>.js` handler.\n\n## Preload bridge & renderer access\n\n`src/preload.js` is the *only* surface the renderer sees. It exposes, via\n`contextBridge.exposeInMainWorld('ipcRenderer', …)`, exactly:\n\n- `invoke(channel, ...args)` → `ipcRenderer.invoke` (renderer→main request/response)\n- `send(channel, ...args)` → `ipcRenderer.send` (fire-and-forget)\n- `on(channel, handler)` → subscribe; **returns an unsubscribe function** and **strips the\n  Electron `event`/`sender` arg** — renderer handlers receive `(...args)`, not `(event, ...args)`\n- `getFilePath(file)` (via `webUtils.getPathForFile`) and `openExternal(url)` (via `shell.openExternal`)\n\nAlso exposed: `window.isPlaywright` (`process.env.PLAYWRIGHT === 'true'`).\n\nRenderer code accesses this as `window.ipcRenderer` (e.g. `const { ipcRenderer } = window;` in\n`bruno-app/src/providers/App/useIpcEvents.js`). **There is no channel allowlist** — any channel\nstring is forwarded. Do not rely on the preload to gate channels; validate inputs inside the\nhandler.\n\n## Channel naming convention\n\nFollow the prefix that matches the direction — the vast majority of handlers already do:\n\n- **`renderer:*`** — renderer→main via `ipcMain.handle` (the default for new work; ~255 of ~290\n  handlers). e.g. `renderer:ready`, `renderer:save-request`, `renderer:create-workspace`,\n  `renderer:browse-directory`, `renderer:get-global-environments`.\n- **`main:*`** — main→renderer push via `webContents.send`. e.g. `main:load-preferences`,\n  `main:collection-opened`, `main:workspace-opened`, `main:collection-tree-updated`,\n  `main:display-error`, `main:app-loaded`. A few `main:*` names are *internal* main-process\n  events routed through `ipcMain.emit`/`ipcMain.on` (`main:renderer-ready`,\n  `main:workspaces-ready`) — see the startup sequence.\n- **Domain-namespaced invokes** for stateful sub-protocols: `grpc:*`, `renderer:ws:*` (WebSocket),\n  `terminal:*`, `menu:*` (menu→main), `internal:*` (main-to-main only).\n- **Legacy unprefixed network channels** — `send-http-request`, `cancel-http-request`,\n  `fetch-gql-schema`, `clear-oauth2-cache`, `connections-changed`. These predate the convention;\n  prefer a `renderer:*` name for new request-side channels rather than adding to this unprefixed set.\n\n## Handler registration\n\nAll handlers are registered inside `app.on('ready')` in `index.js` (the `// register all ipc\nhandlers` block) by calling per-domain `register*Ipc(mainWindow, …)` functions, each `require`d\nat the top of `index.js`. `registerPreferencesIpc` (which kicks off onboarding + the\n`renderer:ready` handler) runs *before* `registerWorkspaceIpc` (which owns `main:renderer-ready`);\nthis is safe because `renderer:ready` only fires after the renderer mounts, by which point every\nhandler is registered. **A new IPC domain must add its own `register*Ipc` call here** — a handler\nfile that is never `require`d + called registers nothing.\n\n## Where new handler code goes — keep it small and testable\n\n**Don't let a handler file keep growing to where its size hurts readability and maintainability.**\nWhen a group of handlers forms a separable feature, give it its own `ipc/<domain>.js` with its own\n`register*Ipc` (wired into `index.js` as above) rather than appending to an existing large file —\nseparate it whenever it makes sense. Splitting by domain is already the norm here (`apiSpec.js`,\n`workspace.js`, `mount.js`, …).\n\n**Structure a handler so its callback is unit-testable.** Write the logic as a named, exported\nfunction and let the thin `ipcMain.handle(channel, fn)` line just call it — not an inline anonymous\narrow, which can only be exercised through Electron. The exported callback can then be tested\nwithout spinning up the main process: see `ipc/swagger-fetch.js` (`proxySwaggerFetch`) and the\nspecs under `ipc/network/` and `ipc/ai/`.\n\n## Error-handling convention\n\nTwo patterns by handler type — match the one that fits:\n\n- **CRUD / lifecycle handlers throw or reject.** Validation failures use bare\n  `throw new Error(...)`; caught errors are re-surfaced with `return Promise.reject(error)` or\n  `throw error`. The renderer `invoke` promise rejects and is shown via a `main:display-error`\n  toast (`useIpcEvents.js`). A few read-only probes intentionally swallow and return a fallback\n  (e.g. `exists-sync` → `false`, `get-collection-workspaces` → `[]`).\n- **Request-execution handlers embed the error in the result.** `send-http-request`\n  (`ipc/network/index.js`) does **not** reject on a failed request/script/test — it returns a\n  result object carrying an `error` field so the renderer can render the failure as data. New\n  network handlers follow this embed-in-result shape; new CRUD handlers throw/reject.\n\n## Security constraints\n\n`BrowserWindow` `webPreferences` (in `index.js`): `nodeIntegration: true`,\n`contextIsolation: true`, `preload: preload.js`, `webviewTag: true`. **This `nodeIntegration:\ntrue` + `contextIsolation: true` combination is intentional — do not \"fix\" it.** No `sandbox`\nkey (defaults off); `webSecurity` defaults on. A CSP is set via `electron-util`'s\n`setContentSecurityPolicy` in `index.js` (`default-src 'self'`; `connect-src` adds PostHog;\n`script-src 'self' data:`; `img-src` allows http/https/blob/data). `form-action 'none'` is\ndeliberately commented out to keep OAuth2 working. External navigation is hardened via\n`will-redirect` + `setWindowOpenHandler`, which route http/https to `shell.openExternal` and deny\neverything else — preserve this when touching window/navigation code.\n\n## IPC startup sequence\n\n```\nrenderer mounts\n  -> useEffect: ipcRenderer.invoke('renderer:ready')    [useIpcEvents.js]\n     -> main handles 'renderer:ready'                    [ipc/preferences.js]\n          await onboardingPromise\n          send main:load-preferences\n          send main:load-global-environments (+ main:git-version, reopen last apispecs, ...)\n          ipcMain.emit('main:renderer-ready')\n            -> workspace.js listener:                     [ipc/workspace.js]\n                 await ensureDefaultWorkspaceExists()\n                 send main:workspace-opened (per workspace)\n                 send + ipcMain.emit('main:workspaces-ready')\n                   -> onboarding.js sends main:collection-opened  [app/onboarding.js]\n\ndid-finish-load (independent, from Chromium):                     [index.js]\n  -> wraps webContents.send with a safeStringify/safeParse JSON roundtrip\n  -> send main:app-loaded\n     -> renderer sink sets data-app-state=\"loaded\"        [pages/Bruno/index.js, NOT useIpcEvents.js]\n```\n\n## IPC race-condition pattern\n\nWhen `ipcMain.emit` triggers multiple `ipcMain.on` listeners:\n- Sync listeners complete fully before the next starts.\n- Async listeners yield at the first `await`, letting the next listener start.\n- `webContents.send` from earlier listeners can arrive at the renderer before messages from\n  later async continuations.\n\nFix: chained events (`main:renderer-ready` → `main:workspaces-ready` → deferred\n`main:collection-opened`) enforce ordering. Reuse this pattern rather than adding `setTimeout`s.\n\n## Persistent stores (`src/store/`, electron-store)\n\nStores are electron-store JSON files under `app.getPath('userData')`, one module per file —\n**read the directory for the current set** (it is large: preferences, cookies, oauth2,\ncollection-security, secrets, workspace/global environments, last-opened-*, window-state, …).\nGotcha: multiple modules can share the *same* JSON file. `lastOpenedCollections` lives in\n`preferences.json` but is owned by `store/last-opened-collections.js` (a distinct\n`new Store({ name: 'preferences' })` module), **not** by `store/preferences.js`. Don't assume a\nstore key belongs to the module named after the file.\n\n## File watchers (`src/app/`)\n\nWatchers live in `src/app/` — **read the directory for the current set** (`collection-watcher.js`,\n`workspace-watcher.js`, `apiSpecsWatcher.js`, `dotenv-watcher.js`). Cleanup is\n**not** uniform:\n- `closeAllWatchers()` in `index.js` tears down exactly three —\n  `collectionWatcher`, `workspaceWatcher`, `apiSpecWatcher` — each of which exposes\n  its own `closeAllWatchers()`.\n- `dotenv-watcher.js` is a shared singleton exposing `closeAll()` and is torn down transitively by\n  the workspace watcher (and driven by the collection watcher), **not** from `index.js`.\n\nWhen adding a watcher, decide which lifecycle it belongs to and register its cleanup in the\nmatching place — a top-level watcher goes into `closeAllWatchers()` in `index.js`; a\nper-workspace/per-collection one hangs off the owning watcher.\n\n## Onboarding flow\n\nEntry: `src/app/onboarding.js` (`onboardUser`), invoked synchronously when `registerPreferencesIpc`\nruns.\n\n1. Checks `preferencesUtil.hasLaunchedBefore()`.\n2. New users: imports the sample collection from `resources/data/sample-collection.json`.\n3. Defers `main:collection-opened` (via a pending-collection var) until `main:workspaces-ready`.\n4. `DISABLE_SAMPLE_COLLECTION_IMPORT === 'true'` skips the import (set in Playwright).\n\n## Key environment variables\n\n- `ELECTRON_USER_DATA_PATH` — custom userData path, **dev mode only** (`index.js:22`, guarded by `isDev`).\n- `DISABLE_SAMPLE_COLLECTION_IMPORT` — skip sample collection on first launch.\n- `PLAYWRIGHT` — signals the app is running under Playwright (read in `preload.js`, stores, etc.).\n- `DISABLE_SINGLE_INSTANCE` — allow multiple app instances.\n\n(All four are set to `'true'` for e2e in `playwright/index.ts`.)\n\n## Before adding or changing an IPC handler — checklist\n\n- [ ] Handler added in `src/ipc/<domain>.js` as `ipcMain.handle('renderer:<verb>', …)` (or the\n      matching domain prefix); no new unprefixed channel.\n- [ ] New handler lives in a focused `ipc/<domain>.js` — a separable feature gets its own file\n      rather than bloating an existing one; its callback is a named exported function\n      (unit-testable), not an inline arrow.\n- [ ] If it's a new domain, its `register*Ipc(mainWindow, …)` is called in `index.js`'s ready block.\n- [ ] Inputs validated inside the handler (the preload has no allowlist).\n- [ ] Error path matches the handler type — CRUD throws/rejects; request-execution embeds `error`\n      in the result.\n- [ ] For a `main:*` push channel, the renderer adds a listener **and** an unsubscribe in\n      `useIpcEvents.js`'s cleanup.\n- [ ] Cross-process values survive the `did-finish-load` JSON roundtrip (no functions,\n      class instances, or circular refs in payloads).\n",".claude/rules/feature-parity.md":"---\npaths:\n  - \"packages/**/*\"\n---\n\n# Co-Related Features & Parity Surfaces\n\nBruno implements the same behaviour more than once — app vs CLI, `.bru` vs `.yml`, filestore vs\nconverters, implementation vs QuickJS shim. **A change to one side of a pair is not a finished\nchange**: the other side silently keeps the old behaviour and ships. The symptom is never a broken\nbuild — it's a feature that works in the app but not in `bru run`, a field that survives save but\nvanishes on export or migration, a `bru.*` method that is `undefined` in the default sandbox.\n\nThis rule maps *which* surfaces pair up. `.claude/rules/dsl-changes.md` owns the on-disk contract\nin depth.\n\n## When you work on a change\n\n1. **Find the twin before writing the fix.** For every file you touch in the tables below, open\n   its counterpart.\n2. **Update the twin in the same change** when it's within what was asked.\n3. **Otherwise, say so explicitly.** If the twin is out of scope, doesn't apply, or you're unsure,\n   list it in your reply to the user (and in the PR description) with the path and why it was left\n   — never leave a one-sided change unmentioned.\n4. **Prefer unifying over copying.** If the logic has no Electron- or CLI-only dependency, move it\n   into `@usebruno/requests` / `@usebruno/common` (respecting `.claude/rules/architecture.md`)\n   instead of patching twice. Precedent: `utils/cookies.js` is one line in each runtime —\n   `module.exports = require('@usebruno/requests').cookies;`. Where both copies must stay, note the\n   pairing in a comment at both sites.\n\n## 1. App ↔ CLI — highest priority\n\n`bruno-cli` is how collections run in CI. Several pipeline modules are **copied, not shared**, and\na bug fixed in the app is still present in `bru run` until its twin is fixed. Trace app-reported\nbugs to the shared logic, not the reporting surface.\n\n| Concern | `bruno-electron/src/…` | `bruno-cli/src/…` |\n|---|---|---|\n| Request execution — pre/post scripts, vars, asserts, tests, runner flow (`setNextRequest`, `skipRequest`) | `ipc/network/index.js` | `runner/run-single-request.js`, `commands/run.js` |\n| Request assembly | `ipc/network/prepare-request.js` | `runner/prepare-request.js` |\n| HTTP client, TLS, certs | `ipc/network/axios-instance.js` | `utils/axios-instance.js` |\n| Interpolation | `ipc/network/interpolate-{vars,string}.js` | `runner/interpolate-{vars,string}.js` |\n| Collection tree, `merge{Headers,Scripts,Vars,Auth}` | `utils/collection.js` | `utils/collection.js` |\n| Collection loading, `bruno.json` / `opencollection.yml` config | `utils/collection-reader.js`, `app/collection-watcher.js` | `utils/collection.js` (`getCollectionConfig`) |\n| Environments & persisted vars | `utils/environments.js` | `utils/environment.js`, `utils/persist-variables.js` |\n| Proxy · OAuth 2.0 · form-data · filesystem | `utils/{proxy-util,oauth2,form-data,filesystem}.js` | `utils/{proxy-util,oauth2,form-data,filesystem}.js` |\n| AWS SigV4 | `ipc/network/awsv4auth-helper.js` | `runner/awsv4auth-helper.js` |\n\n- A new app runner option (tags, delay, bail) needs a `bru run` flag, or a stated reason.\n- Import: the app imports Postman, Insomnia, OpenAPI, WSDL and OpenCollection; `bru import`\n  (`commands/import.js`) only OpenAPI and WSDL. Changes to a shared converter affect both.\n- **Known-divergent by design.** The CLI has no preferences store (so no `resolveInheritedSettings`),\n  no Electron window, no system-browser OAuth, and no `@usebruno/schema` validation. Its runner\n  supports only `http-request` and `graphql-request`: it has no gRPC or WebSocket transport, and\n  every request item goes through the HTTP pipeline in `runner/run-single-request.js`.\n\n## 2. Persisted fields — every layer that carries a field list\n\nA field is added in one place and read in many. Each of these keeps its **own** list, aligned by\nhand:\n\n| Layer | Where | Missed → |\n|---|---|---|\n| Types | `bruno-schema-types/src/collection/item.ts` | type drift |\n| Yup schema | `bruno-schema/src/collections/index.js` — `.noUnknown(true).strict()` | **throws on save** |\n| App save transform | `bruno-app/src/utils/collections/index.js` — `transformRequestToSaveToFilesystem`, `transform{Collection,Folder}RootToSave`, `transformCollectionToSaveToExportAsFile` | dropped before reaching disk / Bruno JSON export |\n| On-disk formats | `bruno-filestore/src/formats/{bru,yml}` (+ `bruno-lang`) — parse **and** stringify | lost in one format |\n| BRU → YML migration | `bruno-electron/src/ipc/yml-migration.js` (bru parse → yml stringify) | lost when a user migrates |\n| Collection config | `bruno.json` ↔ `opencollection.yml` — `utils/transformBrunoConfig.js`, `stringifyCollection`, migration's config transform (e.g. `transformProxyConfig`), CLI `getCollectionConfig` | config lost or misread by CLI |\n| OpenCollection import/export | `bruno-converters/src/opencollection/items/{http,graphql,grpc,websocket}.ts`, `folder.ts`, `environment.ts` — **both** directions; app entry `bruno-app/src/utils/exporters/opencollection.js` | lost on export or import |\n| Other converters | `postman/{postman-to-bruno,bruno-to-postman}.js`, `insomnia/`, `openapi/`, `wsdl/`; app exporters `postman-collection.js`, `openapi-spec.js`, `bruno-environment.js` | lost on import/export where the target can represent it |\n\nThe migration also rewrites content, not just shape: `bru.runRequest(\"x.bru\")` paths are stripped\nof `.bru`, and tab snapshots are remapped. A new script API that takes a file path, a new file\ntype, or a new `bruno.json` key needs a matching migration step.\n\n## 3. Scripting API ↔ QuickJS shim\n\n`bruno-js/src/{bru,bruno-request,bruno-response}.js` implement the API. The **QuickJS sandbox — the\ndefault \"safe\" mode — bridges each method explicitly** in `bruno-js/src/sandbox/quickjs/shims/`\n(`bru.js`, `bruno-request.js`, `bruno-response.js`, `bruno-grpc.js`, …). Implementation only →\nworks in developer mode, `undefined` for most users.\n\n## 4. Scope levels — collection ↔ folder ↔ request\n\nAuth, headers, vars, scripts and tests exist at three levels plus `inherit`. Each level has its own\nUI (`CollectionSettings/`, `FolderSettings/`, `RequestPane/`), and resolution lives in the merge\nhelpers in **each** runtime's `utils/collection.js`. A feature at one level needs a stated decision\nfor the other two and for `inherit`.\n\n## 5. Protocols — HTTP ↔ GraphQL ↔ gRPC ↔ WebSocket\n\n`REQUEST_TYPES` (`bruno-app/src/utils/common/constants.js`, `bruno-electron/src/utils/constants.js`)\nlists all four. Each has its own `parse*`/`stringify*` in `bruno-filestore/src/formats/yml/items/`\nand its own converter in `bruno-converters/src/opencollection/items/`. \"HTTP only\" is a legitimate\nscope — leaving it unstated is how a gap becomes a bug report. Widening CLI protocol support is a\ndeliberate decision, not an incidental one.\n\n## 6. Request shaping ↔ code generation\n\n`GenerateCodeItem/utils/snippet-generator.js` consumes headers, body, `settings` and resolved auth.\nA new auth mode or a setting that changes the wire request should change the generated snippet.\n\n## Parity checklist\n\n- [ ] Every twin in §1 opened; updated, or listed to the user with a reason\n- [ ] App-reported bug fixes verified on the CLI path\n- [ ] Shared logic moved to a leaf package where the DAG allows, instead of patched twice\n- [ ] Persisted field carried through every layer in §2 — including migration and converters (both directions)\n- [ ] New/changed `bru.*`, `req.*`, `res.*` API bridged in the QuickJS shim\n- [ ] Collection / folder / request levels and `inherit` each decided\n- [ ] gRPC and WebSocket considered; any \"HTTP only\" scope stated\n- [ ] Code generation updated if the wire request changed\n- [ ] Tests on **both** sides of every pair touched\n\nHuman-facing version of this map (for QA): Confluence *Related Feature Map — Change Impact &\nParity Checklist*, https://usebruno.atlassian.net/wiki/spaces/Bruno/pages/1376944136. Keep the two\nin step: add new pairings to both, and remove unified ones from both.\n",".claude/rules/redux-store.md":"---\npaths:\n  - \"packages/bruno-app/**/*\"\n---\n\n# React / Redux Architecture\n\n## Store configuration\n\n`configureStore` (Redux Toolkit **v1.8**, not v2) is set up in\n`src/providers/ReduxStore/index.js`. That file's `reducer` map is the **authoritative slice list**\n(~11 keys); read it rather than trusting any list here. Middleware is\n`getDefaultMiddleware().concat([...])` with **no options passed**, so RTK's defaults —\n`thunk`, plus `serializableCheck` and `immutableCheck` in dev — are all active.\n\n**Consequences to respect:**\n- **State must stay serializable.** Don't put functions, class instances, Promises, or DOM nodes\n  in the store — dev `serializableCheck` will flag them. Do **not** disable `serializableCheck` in\n  the real store to work around it (the only place it's disabled is a middleware unit test).\n- **Reducers mutate the Immer draft directly.** `createSlice` reducers write\n  `state.collections.push(...)`, `item.name = ...`, `.splice(...)` — do **not** spread-clone or\n  return new objects unless replacing wholesale. Match the surrounding reducer.\n\n## Slice organization\n\nSlices live in `src/providers/ReduxStore/slices/`, one domain each; `collections/` is by far the\nlargest (collection tree, requests, environments, runner, drafts — ~200 reducer action creators in\n`collections/index.js`, plus ~150 thunks in `collections/actions.js`, and more spread in from\n`collections/exampleReducers.js`). Read the specific slice for its real state and actions.\n\nTwo gotchas:\n- **Reducer key ≠ folder/file name.** e.g. `globalEnvironments` comes from\n  `slices/global-environments.js`; `openapiSync` from `slices/openapi-sync.js`.\n- **Action-type prefix follows the slice's `name:`, not the reducer key.** e.g.\n  `global-environments/…`, `collections/…`, `app/…`. Grep the action type, not the key, when\n  tracing dispatches.\n\n## Actions / thunks convention\n\nAsync logic is written as plain hand-written thunks — a function that returns\n`(dispatch, getState) => …`. `createAsyncThunk` isn't used anywhere today:\n\n```javascript\nexport const saveRequest = (itemUid, collectionUid) => (dispatch, getState) => { ... };\n// or async\nexport const openCollectionEvent = (uid, pathname, ...) => async (dispatch, getState) => { ... };\n```\n\nThunks live in the slice's `actions.js` (`slices/collections/actions.js` is the big one:\n`sendRequest`, `saveRequest`, `importCollection`, `pasteItem`, `deleteItem`, `openCollectionEvent`\n— the last handles the `main:collection-opened` IPC). Reducers (in the slice's `index.js`) stay\nsynchronous. Follow the plain-thunk shape by default so new code matches the rest — but this is a\nconvention, not a blocker: if a feature genuinely benefits from `createAsyncThunk` (e.g. its\nbuilt-in pending/fulfilled/rejected states), it's fine to reach for it deliberately.\n\n## Side-effect boundaries\n\n- **Reducers are pure** — no IPC, no network, no `Date.now`/random, no async. Immer draft\n  mutations only.\n- **Side effects live in thunks and middleware.** IPC is invoked from thunks via\n  `window.ipcRenderer` (e.g. `saveRequest` → `ipcRenderer.invoke('renderer:save-request', …)`); the\n  `collections/actions.js` file alone has ~200 `ipcRenderer` calls.\n- **The file-IO → open-tab pattern** is split across a thunk and the `tasks` middleware: a thunk\n  queues an `OPEN_REQUEST`/`OPEN_EXAMPLE` task (`insertTaskIntoQueue`) and fires the IPC write; the\n  `tasks` middleware watches for the resulting `main:*` file event, then dispatches `addTab` +\n  `removeTaskFromQueue`. Reuse this queue rather than opening the tab optimistically.\n\n## Custom middleware (`middlewares/`)\n\nFive middlewares, registered in `index.js` (read the directory + registration array for the live\nset):\n- **`tasks`** — listener middleware; opens a request/example tab after the file event for a queued\n  task arrives (see file-IO pattern above).\n- **`draft`** — on the first edit action, promotes a *preview* tab to permanent\n  (`handleMakeTabParmanent`). It does **not** write draft state — the reducers do.\n- **`autosave`** — debounced by `preferences.autoSave.interval`, gated on\n  `app.preferences.autoSave.enabled`; dispatches the `save*` thunks.\n- **`snapshot`** — debounced (~1s), gated on `app.snapshotReady`; persists a curated state subset\n  to disk (see Persistence).\n- **`debug`** (dev only) — logs every action; added only when `import.meta.env.MODE ===\n  'development'`.\n\n## Persistence / serialization constraints\n\nThe store isn't persisted wholesale, and there's no `redux-persist`. Instead the `snapshot`\nmiddleware serializes a **curated subset** — workspaces, collection metadata, per-collection tabs,\nselected environments, and devtools console open-state (`extras.devTools`, from the `logs` slice;\nsee `middlewares/snapshot/serializeSnapshot.js`) — and writes it to disk via IPC. Request/response\nbodies are deliberately not persisted. To persist a new piece of state, extend `serializeSnapshot`\nand its save-trigger list (`utils/snapshot`) — the snapshot is the established mechanism, so prefer\nit over adding `redux-persist` or `localStorage`.\n\n## Selectors\n\nInline `useSelector` in components is the norm (hundreds of usages); memoized `createSelector` is\nused in just one place (`src/selectors/tab.js`). Default to inline\n`useSelector`, and reach for `createSelector` when a derivation is genuinely expensive or reused.\n\n## What belongs in Redux (and what doesn't)\n\n- **In Redux:** draft / unsaved state (`item.draft`, `collection.draft`, `environmentsDraft`,\n  `globalEnvironmentDraft`), and cross-component or persisted UI state (`sidebarCollapsed`,\n  `leftSidebarWidth`, `taskQueue`, `item.collapsed`, active tab).\n- **Local `useState`:** ephemeral, component-scoped UI — search text, inline-create toggles, hover,\n  transient form state. Don't lift these into a slice.\n\n## Sidebar collection visibility\n\nCollections render in the sidebar only when they belong to the **active workspace**.\n`Sidebar/Collections/index.js` builds the list in a `useMemo` that walks\n`activeWorkspace.collections`, matches loaded collections through a `Map` keyed on\n`normalizePath(pathname)`, **excludes scratch collections** (`isScratchCollection`), adds \"ghost\"\ngit-remote rows (`GitRemoteCollectionRow`) for non-default workspaces, and sorts alphabetically.\nRead the current `useMemo` before changing this. Collapsed folders do **not** render their children\nin the DOM (conditional render on `item.collapsed`).\n\n## Sidebar DOM structure (for Playwright)\n\n`.collection-item-name` is the class on the **row-wrapper** div for every item (folders, requests,\nJS files) in `Sidebar/Collections/Collection/CollectionItem/index.js` — the wrapper also contains\nthe name span and `.menu-icon`. Use it for Playwright locators.\n\n## Providers (`src/providers/`)\n\nRead the directory for the current set: `App/` (IPC listeners in `useIpcEvents.js`, app init),\n`ReduxStore/`, `Theme/`, `Hotkeys/`, `Toaster/`, `PromptVariables/`.\n\n## Checks after changing a slice / middleware\n\nSlices and middleware have Jest coverage under `src/providers/ReduxStore/` (e.g.\n`slices/collections/*.spec.js`, `slices/global-environments.spec.js`,\n`middlewares/snapshot/serializeSnapshot.spec.js`). After a change, run the relevant spec:\n\n```bash\nnpm test --workspace=packages/bruno-app -- <path-or-pattern>\n```\n\nIf you touch the reducer map, thunks that fire IPC, or the snapshot subset, also sanity-check that\nstate stays serializable (no dev `serializableCheck` warnings) and that persisted shape still\nround-trips through `serializeSnapshot`.\n",".claude/rules/sqlite.md":"---\npaths:\n  - \"packages/bruno-sqlite/**/*\"\n  - \"packages/bruno-electron/src/ipc/sqlite.js\"\n  - \"packages/bruno-electron/src/services/sqlite/**/*\"\n---\n\n# SQLite SDK (`@usebruno/sqlite`)\n\nBruno's local database layer. You author **migrations** (`migrations/*.ts`) and **statements**\n(`statements/*.sql`); a codegen step compiles them into a typed data layer for the Electron main\nprocess, which owns the database and executes statements. The package has no renderer entry point:\nthe renderer reaches a statement only through an IPC handler written by hand for that statement in\n`bruno-electron`.\n\n**The renderer never sends SQL, and never picks the statement.** Each IPC channel is bound to exactly\none statement in the main process; the renderer sends only that statement's params. Do not add an\nIPC handler, statement, or option that accepts SQL text or a statement name from the renderer.\n\n## When to use it — and when not to\n\nUse it for state that is **derived, bulky, or secondary**: data Bruno can rebuild or lose without\ncorrupting anything the user authored, and that would bloat Redux or the IPC payloads if held in\nmemory.\n\nIt is **not** the right home for:\n\n- **Anything the user authored.** Collections, requests, environments, folders and `bruno.json` live\n  on disk as `.bru`/`.yml` — see `.claude/rules/dsl-changes.md`. Never mirror or migrate a\n  serialized field into SQLite.\n- **App preferences and small keyed state** — those are electron-store JSON files under\n  `src/store/` (see `.claude/rules/electron-ipc.md`).\n- **UI state** — Redux, or local `useState` (see `.claude/rules/redux-store.md`).\n\nThe database file is `bruno.db` under `app.getPath('userData')`, and the file store spills large\npayloads to `sqlite-files/` beside it. **Treat both as disposable**: the open path below deletes\nand rebuilds the database rather than failing (startup `collect()` then sweeps the spilled files no\nrow names), so never let either hold the only copy of anything.\n\n## Package layout\n\n```\npackages/bruno-sqlite/\n  migrations/<seq>_<name>.ts   authored — exports up()/down() returning SQL\n  statements/*.sql             authored — sqlc-style annotated statements\n  scripts/                     codegen (generate-artifacts), new-migration, verify-migrations,\n                               generate-schema\n  SCHEMA.md                    GENERATED, committed — ER diagram + table schemas\n  src/node/                    db.ts (migrations), statements.ts (prepare/execute)\n  src/shared/                  shared types\n  src/generated/               GENERATED, gitignored — never edit or commit\n  tests/node\n```\n\n**`@usebruno/sqlite` (= `/node`) is main-process only** (it imports `node:sqlite`, `node:fs`,\n`node:crypto`); never import it from the renderer. The package has zero internal `@usebruno/*`\ndependencies and must stay that way.\n\n`src/generated/` is produced from the authored migration and `.sql` files by `npm run generate`\n(which `prebuild`/`pretest` run for you). Those authored files are the only source of truth.\n\n## Adding a migration\n\n```bash\nnpm run migration:new --workspace=packages/bruno-sqlite    # prompts for a name\n```\n\nThis scaffolds `migrations/<7-digit sequence>_<name>.ts` exporting `up` and `down`, each returning\nthe SQL to apply or revert:\n\n```ts\nexport const up = (): string => {\n  return 'CREATE TABLE users (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, email TEXT);';\n}\nexport const down = (): string => {\n  return 'DROP TABLE users;';\n}\n```\n\nMigrations run at open time, in sequence order, each in its own transaction, and are recorded in a\n`_migrations` table with a sha256 of both `up` and `down`.\n\n- **A merged migration is immutable.** Editing one changes its hash; the next open throws\n  `does not match the migration already applied`, and `createDatabase` responds by **deleting the\n  user's database and rebuilding it**. Add a new migration instead — always.\n- **`down` must genuinely revert `up`.** It is not decoration: when the tail migration disappears\n  from the code (a user downgrading Bruno, or you switching branches), the recorded `down` is\n  executed automatically against the existing database. An empty or wrong `down` corrupts that path.\n- Only the **tail** rolls back (recorded sequences above the highest local one). Deleting a\n  migration from the middle silently leaves its schema in place — don't.\n- Design schema changes to tolerate an older Bruno opening the same file afterwards: prefer new\n  tables and nullable columns over reshaping an existing one.\n\nVerify against a real database rather than an empty one — this replays every migration on a\nthrowaway `VACUUM INTO` copy and then prepares every statement against the result:\n\n```bash\ncp .env.example .env    # set DB_PATH to an absolute path to a real bruno.db\nnpm run migration:verify --workspace=packages/bruno-sqlite\n```\n\n`SCHEMA.md` documents the resulting schema: a Mermaid ER diagram plus each table's columns, keys and\nindexes, produced by applying every migration to an empty in-memory database. The pre-commit hook\nregenerates and stages it whenever a file under `migrations/` is staged; run\n`npm run schema --workspace=packages/bruno-sqlite` to regenerate it by hand. Never edit it directly.\n\n## Adding statements\n\nStatements live in `statements/*.sql`, several per file, each introduced by a sqlc-style\nannotation. Params are **named** and always passed as an object — there is no positional binding:\n\n```sql\n-- name: list_users :many\nSELECT id, name, email FROM users ORDER BY name;\n\n-- name: get_user :one\nSELECT id, name, email FROM users WHERE id = @id;\n\n-- name: create_user :exec\nINSERT INTO users (name, email) VALUES (@name, @email);\n```\n\n| annotation | runs as | returns |\n|---|---|---|\n| `:one` | single row | row or `undefined` |\n| `:many` | rows | array |\n| `:exec` | write | `{ changes, lastInsertRowid }` |\n\n- Statement names are **globally unique across all `.sql` files** (a duplicate fails codegen).\n  Renaming a statement, or moving it to another file, means renaming its IPC channel and every\n  renderer call site too.\n- Use `@param` for consistency with the existing statements, even though `node:sqlite` also accepts\n  `:name` and `$name`.\n\n## Main process\n\n`packages/bruno-electron/src/services/sqlite/index.js` owns the single database instance:\n`openDatabase()` calls `createDatabase` on `bruno.db` and starts a background `files.collect()`; the\nservice exposes `getStatements()`, `getFiles()` (the SDK's file store — see the package README),\n`transaction(callback)`, `reclaimDiskSpace()` and `shutdown()`. It never hands out the `DB` itself,\nand it knows nothing about IPC. `index.js`'s ready block opens it (`sqliteService.openDatabase()`),\nand `before-quit` reclaims disk space and shuts it down.\n\n```js\nconst { getStatements, transaction } = require('../../services/sqlite');\n\ntry {\n  transaction(() => {\n    getStatements().execute('create_user', { name, email });\n  });\n} catch (err) {\n  // degrade: the database may be unavailable\n}\n```\n\n`getStatements()` never returns `null`. Before `openDatabase()`, after shutdown, and when\n`createDatabase` returns `{ db: undefined, statements: undefined }` (even the in-memory fallback\nfailed), it returns a stub whose `execute` logs the skipped statement and throws; `getFiles()`\nreturns a matching stub whose methods log and throw (or reject, for the async ones), and\n`transaction` throws the same way. So there is one failure mode, and every caller must handle it:\n\n- **`execute` and `transaction` throw** when the database is unavailable, for an unknown statement,\n  for one that couldn't be prepared against this schema (prepare failures are logged at construction\n  and only that statement is discarded — the rest of the database stays usable), and for any SQL\n  error. Wrap calls in try/catch and stay useful without the database — degrade, don't crash.\n\n`createDatabase` degrades rather than failing: file → (migration error) delete the `bruno.db*` files\nand rebuild → in-memory → `undefined`. A real backup is still a `TODO` in `src/node/index.ts`; it\ndeletes today.\n\n**`node:sqlite` is synchronous — every `execute` blocks the main process.** Keep statements indexed,\nbounded, and off hot paths; no full-table scans, no unbounded result sets, no per-event writes in a\ntight loop.\n\n## Exposing a statement to the renderer\n\nOnly statements the renderer actually needs are exposed, each with its own hand-written\n`ipcMain.handle` in `packages/bruno-electron/src/ipc/sqlite.js`. `registerSqliteIpc()` is called in\n`index.js` right after `openDatabase()`; each handler calls the service's `getStatements()` per\ninvocation, so a call after shutdown hits the stub, not a closed database.\n\nThe channel name follows a fixed convention: **`datastore:<statement_file>:<statement_name>`**, where\n`<statement_file>` is the `.sql` file's name without the extension, written exactly as the file is\nnamed. Channel names are written by hand, not generated.\n\n```js\nipcMain.handle('datastore:runner_responses:get_runner_response', (_event, params) => {\n  const request_uid = requireUid(params?.request_uid, 'request_uid');\n  return getStatements().execute('get_runner_response', { request_uid });\n});\n```\n\n- The handler validates and picks the params it forwards — never pass the renderer's object\n  straight through to `execute`.\n- The file store has no channels of its own. File ids are sequential and unscoped, so a channel\n  that takes one would expose every stored blob; the renderer reaches files only through a\n  domain handler that resolves them from a scoped key (e.g. `get_runner_response` by\n  `request_uid`). File writes stay main-process only — the renderer never chooses what gets\n  stored.\n- In the renderer, wrap each channel in a named hook or helper under `src/hooks/useX/index.js`\n  (or the relevant util) and call `window.ipcRenderer.invoke(channel, params)` there; no component\n  invokes a `datastore:*` channel directly.\n\n## Commands\n\n```bash\nnpm run build:bruno-sqlite                          # generate + rollup (from the repo root)\nnpm run watch --workspace=packages/bruno-sqlite     # rebuild on change\nnpm run test  --workspace=packages/bruno-sqlite     # jest; sets --experimental-sqlite for you\n```\n\n`npm run dev` does **not** rebuild this package, and bruno-electron consumes its `dist/` — after\nediting anything under `packages/bruno-sqlite`, rebuild or run the watcher or the app keeps the old\nstatements. Run the tests through the npm script: bare `jest` misses the\n`NODE_OPTIONS=\"--experimental-sqlite\"` the repo's Node needs for `node:sqlite`.\n\n## Before you call it done\n\n- [ ] New persistence genuinely belongs here — not `.bru`/`.yml`, electron-store, or Redux\n- [ ] No merged migration edited; new schema added as a new migration with a working `down`\n- [ ] `npm run generate` output shows the expected statements\n- [ ] `migration:verify` run against a real `bruno.db` copy\n- [ ] Main-process callers try/catch `execute` / `transaction`, and still work when the database\n      is unavailable\n- [ ] Each statement the renderer needs has its own `ipcMain.handle` on\n      `datastore:<statement_file>:<statement_name>`; renderer access goes through a named hook\n- [ ] Package rebuilt (`npm run build:bruno-sqlite`) before testing in the app\n- [ ] `npm run test --workspace=packages/bruno-sqlite` plus the affected app/electron specs\n",".claude/rules/testing.md":"---\npaths:\n  - \"tests/**/*\"\n  - \"playwright/**/*\"\n  - \"playwright.config.ts\"\n---\n\n# Playwright E2E Testing Guide\n\n> **Reference:** For the full narrative guide — test structure, the page-module locator/action pattern, and worked examples — see [`docs/playwright-testing-guide.md`](../../docs/playwright-testing-guide.md). This file is the terse, path-scoped quick reference; the guide is the canonical source for conventions.\n\n## Running Tests\n\n```bash\nnpm run test:e2e                           # default + system-pac + mock-server (starts dev servers automatically)\nnpx playwright test tests/request/         # Run specific directory\nnpx playwright test --project=default      # Run specific project\nnpx playwright test --headed               # Watch mode\n```\n\nProjects: `default` (main), `system-pac` (depends on `default`), `mock-server`, `auth`, `ssl`.\n`npm run test:e2e` runs `default` + `system-pac` + `mock-server`; `auth` and `ssl` have their own\n`test:e2e:*` scripts and CI jobs. Config: `playwright.config.ts` — `fullyParallel: true`, `workers` unset\n(Playwright default, not single-worker), retries 0 local / 2 CI.\n\n## Test Fixtures (playwright/index.ts)\n\nSee `playwright/index.ts` for the current set; the table covers the common ones.\n\n| Fixture | Scope | Purpose |\n|---------|-------|---------|\n| `page` | test | Default page from `electronApp` |\n| `electronApp` | worker | Default Electron launch (skips onboarding) |\n| `launchElectronApp(opts)` | worker | Custom launch with userData, env, init data |\n| `reuseOrLaunchElectronApp(opts)` | worker | Caches app instances by key |\n| `pageWithUserData` | test | Loads `init-user-data/` from test dir, waits for app ready |\n| `createTmpDir(tag?)` | worker | Temp directory, auto-cleaned after worker |\n| `collectionFixturePath` | test | Copies `fixtures/collection(s)/` to temp dir |\n| `restartApp(opts)` | test | Restart with fresh user data |\n| `newPage` | test | Fresh app instance with tracing |\n| `installFakeClipboard(page)` | test | Replaces `navigator.clipboard` on that page for the test; `copiedText()` returns what the app copied |\n\n## Test Data Conventions\n\n- `tests/<suite>/init-user-data/` — Seed data for existing-user tests (`pageWithUserData`)\n- `tests/<suite>/init-user-data-fresh/` — Seed data for new-user tests (hasLaunchedBefore: false)\n- `tests/<suite>/fixtures/collection(s)/` — Collection fixtures auto-copied to temp dirs\n- `initUserDataPath` files support `{{projectRoot}}` template variable\n- No `initUserDataPath` → default prefs with `hasLaunchedBefore: true` (skip onboarding)\n- `dotEnv` overrides defaults (e.g., `DISABLE_SAMPLE_COLLECTION_IMPORT: 'false'`)\n- Adding/changing a key in the app's `preferences.json` defaults? Mirror it in the `defaultPreferences` mock in `playwright/index.ts` — see [the guide](../../docs/playwright-testing-guide.md#3-keep-defaultpreferences-in-sync-with-app-preferences).\n\n## Test Helpers (tests/utils/)\n\nReuse existing helpers instead of hand-rolling clicks; read the module for its current exports\n(e.g. `grep \"^export\" tests/utils/page/actions.ts`). Modules by responsibility:\n\n- `page/actions.ts` — high-level user actions (create/open collections, requests, folders, environments; send/save requests; workspace + tab management; import)\n- `page/locators.ts` — locator builders (`buildCommonLocators(page)` + per-protocol/feature variants) and table helpers\n- `page/runner.ts` — collection/folder runner actions and result assertions\n- `tests/utils/cli.ts` — `runCLI(cwd, args)` for bruno-cli testing\n- `tests/utils/wait.ts` — polling helpers (e.g. `waitForPredicate(predicate, {tries, interval})`)\n\n**Convention:** a spec must never inline raw selectors. Each `page/*` module owns one UI section and exports both a locator builder and its actions; a new section gets a new file whose builder is mapped into `buildCommonLocators`. Full pattern + examples in [the guide](../../docs/playwright-testing-guide.md#1-centralize-locators-and-actions-in-page-modules).\n\n## Waiting for App Ready\n\n```javascript\nawait page.locator('[data-app-state=\"loaded\"]').waitFor();\n```\n\nThis attribute is set when `main:app-loaded` IPC arrives (independent of `renderer:ready` chain).\n\n## Mutating Fixtures\n\nThe suite must be isolated (unique tmp paths, no shared state).\n\nMost specs load fixtures via `collectionFixturePath` (copies `fixtures/collection(s)/` to a temp dir) or `pageWithUserData` (loads `init-user-data/` into a fresh temp userData dir). These operate on **temp copies** — they need **no** cleanup, even though they mutate files on disk (e.g. persistence specs writing to `environments/*.bru` or `collection.bru`). Only a spec that mutates a **committed** fixture **in place** must restore it in `afterAll` — those use `git checkout <fixturePath>`. Don't flag a missing `afterAll` on a temp-copy spec.\n\n## Common Pitfalls\n\n1. **Worker-scoped fixtures persist across test retries** — app state from failed attempts carries over. If test creates resources (folders, collections), retries may fail trying to create duplicates.\n\n2. **Collapsed folders hide children from DOM** — `toHaveCount`/`toBeVisible` won't find items inside collapsed folders. Expand folders before asserting on their children.\n\n3. **Sequential tests sharing state** — Tests in the same `describe` that depend on prior test state will cascade-fail. The `afterAll` cleanup only runs once, not between retries.\n\n4. **File watcher timing** — After creating files via IPC, the collection watcher must detect and process the change before the UI updates. Use Playwright's auto-retrying assertions (e.g., `expect(locator).toBeVisible()`) rather than immediate checks.\n\n5. **Default env vars in fixture** — `launchElectronApp` sets `DISABLE_SAMPLE_COLLECTION_IMPORT: 'true'` by default. Tests that need onboarding must override via `dotEnv`.\n\n6. **The renderer outlives the test** — `electronApp` is worker-scoped and `page` hands back the same window, so anything a spec patches into the page (a global, a stubbed browser API) leaks into later specs in that worker. Use a fixture that restores on teardown; the real clipboard is also shared across workers, so assert copies through `installFakeClipboard(page)`, not `navigator.clipboard.readText()`.\n\n## Test Coverage Areas\n\nTest suites live under `tests/`, one directory per feature area — list that directory for the\ncurrent set. Examples: `request/`, `collection/`, `environments/`, `runner/`,\n`scripting/`, `graphql/`, `grpc/`, `websockets/`, `auth/`, `ssl/`.\n",".claude/skills/code-review/SKILL.md":"---\nname: code-review\ndescription: Review a Bruno diff, PR, or branch via focused reviewers run in parallel —\n  correctness, security, DSL, React, cross-platform, tests. Mirrors the CodeRabbit /\n  CODING_STANDARDS review.\n---\n\n# Reviewing Bruno code\n\nThis skill mirrors the automated CodeRabbit review (`.coderabbit.yaml`) so you can run\nthe same review locally. Source-of-truth order: coding standards → `CODING_STANDARDS.md`;\narchitecture/behavior → `.claude/rules/*`. `.coderabbit.yaml` mirrors these for CI parity —\nread it for a path instruction not summarized here, but the rules win if they ever disagree.\n\nThe review is **split into focused lenses that run in parallel**, so a large diff is\ncovered faster and each reviewer stays sharply scoped to one concern. Each lens lives in\nits own file under `reviewers/`. You (the orchestrator) dispatch the reviewers, then\nmerge and report their findings.\n\n## How to review (orchestration)\n\n1. **Get the diff.** Review only what changed — never untouched code. Pick the mode:\n   - **Committed range** (default): `git diff main...HEAD` against the base branch\n     (`main` or `release/*`). Update the base first (`git fetch` and confirm the local\n     base is current) — a stale base inflates the diff with already-merged, unrelated\n     changes and wastes a full fan-out. Each reviewer re-runs this scoped to its own\n     globs; since the range is pinned to fixed commits they all see identical bytes, so\n     there's no need to snapshot.\n   - **Working tree / uncommitted changes** (when asked to review unstaged or staged\n     work): the tree can shift mid-review, so re-running `git diff` per reviewer risks\n     each seeing a different snapshot. Instead, capture the diff **once** to a scratch\n     file and hand every reviewer that path. `git diff HEAD` covers staged + unstaged\n     tracked changes; run `git add -N .` first (reversible with `git reset`) so any new\n     untracked files also show up:\n     ```bash\n     git add -N . && git diff HEAD > \"$SCRATCH/review.diff\"\n     ```\n     `$SCRATCH` is your environment's scratchpad dir. Reviewers read this frozen diff for\n     their globs plus the on-disk files for surrounding context — the working-tree files\n     already hold the uncommitted state.\n2. **Enumerate changed files** — `git diff --name-only main...HEAD` (committed range) or\n   `git diff --name-only HEAD` (working tree). Use this to skip any lens whose file scope\n   isn't touched (e.g. no `packages/bruno-app/**` change → skip `react.md`; no `tests/**`\n   change → skip `e2e-tests.md`). Never skip the lenses scoped to all files, and never skip\n   `feature-parity.md` for a small diff — a one-file change is exactly the shape it catches.\n3. **Fan out the reviewers in parallel.** In a *single message*, launch one `Agent`\n   (subagent_type `Explore` or `general-purpose`) per in-scope reviewer below. Give each\n   subagent this exact briefing:\n   - The diff source — the committed range (e.g. `main...HEAD`) or the snapshot file path\n     (`$SCRATCH/review.diff`) — and the file globs it owns (from the reviewer file's\n     \"Scope\" line). For a snapshot, tell the reviewer to read that file for its globs\n     rather than re-run `git diff`.\n   - \"Read `.claude/skills/code-review/reviewers/_contract.md` for the shared persona and\n     output contract, then read `.claude/skills/code-review/reviewers/<file>` — and any rule\n     or source file it points to (e.g. `CODING_STANDARDS.md`, `.claude/rules/*.md`), which\n     hold the detailed checklist. Apply **only** that lens to the changed files in your\n     scope, at the severities the reviewer specifies. Do not review outside your scope.\"\n   Reviewers are read-only and independent — they don't coordinate, and overlap between\n   lenses is fine (you dedupe at merge time).\n4. **Merge and report.** Collect every reviewer's findings, drop exact duplicates, and\n   when two lenses flag the same `file:line` keep the higher severity. Regroup **by\n   file**, each finding tagged by severity (blocker / suggestion / nit) with `file:line`.\n   If the review request carries a problem statement or acceptance criteria (e.g. passed\n   as args), reconcile its enumerated deliverables — docs, migration notes, a test per new\n   default/branch — against the diff and flag any that are absent. If nothing's wrong, say\n   so briefly — don't manufacture nits.\n\n## Reviewers\n\nEach file is a self-contained checklist for one lens:\n\n| Reviewer file | Lens | Scope |\n|---|---|---|\n| `reviewers/correctness.md` | Correctness & root-cause | all source (excl. `tests/**`) |\n| `reviewers/architecture.md` | Architecture & dependency boundaries | `packages/**` |\n| `reviewers/conventions.md` | Coding standards & readability | all files |\n| `reviewers/react.md` | React — app files | `packages/bruno-app/**` |\n| `reviewers/cross-platform.md` | Cross-platform (macOS/Windows/Linux) | all files |\n| `reviewers/security.md` | Security & data safety | all source (excl. `tests/**`) |\n| `reviewers/dsl-changes.md` | On-disk DSL & serialization (backward compat) | `bruno-app`, `bruno-electron`, `bruno-cli`, `bruno-lang`, `bruno-filestore`, `bruno-schema(-types)`, `bruno-converters` |\n| `reviewers/feature-parity.md` | Co-related features & parity (app↔CLI, shim, converter, level, protocol twins) | `packages/**` |\n| `reviewers/e2e-tests.md` | Playwright E2E tests | `tests/**` |\n\n## Shared reviewer persona & output contract\n\nDefined once in `reviewers/_contract.md` — the small file every reviewer reads (dispatched\nreviewers read it, not this orchestration file). Keep the persona and the\n`<blocker|suggestion|nit> | <file>:<line> | <finding>` output shape there, not duplicated here,\nso a change updates a single place.\n",".claude/skills/code-review/reviewers/_contract.md":"# Shared reviewer persona & output contract\n\nRead by every `code-review` reviewer. (Orchestration lives in `../SKILL.md`; the per-lens\nchecklists live in the sibling `*.md` files.)\n\n**Persona** — an expert reviewer in TypeScript, JavaScript, Node.js, and Electron on an\nenterprise team. Be **concise**: one clear sentence per finding; elaborate only when asked.\nReview to the project's standard regardless of who authored or requested the change — never\nsoften severity for assumed intent or seniority. Ground every finding in the actual code: trust\nthe repo over any doc, guide, or comment when they disagree, and never cite a line or invent an\nexample value you haven't verified in source.\n\n**Output contract** — return a flat list, one finding per line:\n\n```\n<blocker|suggestion|nit> | <file>:<line> | <one-sentence finding>\n```\n\nReturn `no findings` (nothing else) when the scope is clean. Never invent nits to fill the list.\n",".claude/skills/code-review/reviewers/architecture.md":"# Architecture & dependency-boundaries reviewer\n\n**Scope:** `packages/**`.\n\nAdopt the reviewer persona and return findings in the output contract defined in\n`_contract.md`.\n\nReview the diff against **`.claude/rules/architecture.md`** — read its \"Dependency direction &\nownership boundaries\" section, plus \"Declared dependencies must match real imports\" when the diff\ntouches a `package.json`. The `@usebruno/*` packages form a strict dependency DAG and some carry hard\nplatform constraints. Report violations with `file:line`, severity:\n\n- **blocker** — a new internal `@usebruno/*` dependency that points *upward* or forms a cycle (a\n  shared lib importing `@usebruno/app` or `@usebruno/electron`); `bruno-common` importing a Node\n  built-in (`fs`/`path`/`os`/`crypto`/`child_process`/`node:*`) or any other `@usebruno/*` package\n  (it must stay browser-safe and dependency-free); `bruno-js` importing `electron`/IPC (it also runs\n  in the CLI); a runtime import of `bruno-schema-types` (types-only).\n- **suggestion** — code placed in the wrong package/layer: renderer-only logic in a shared lib,\n  host/Electron wiring pushed into `bruno-js`, or a util added to `bruno-common` that needs another\n  package; a new shared dependency that would be better kept local.\n- **suggestion** — manifest drift breaking the rule's **Declared dependencies must match real\n  imports**, read against the importing package's own `package.json`.\n- **Not a finding:** dependency edges already in the DAG, or a new *downward* dependency that fits\n  the established direction.\n",".claude/skills/code-review/reviewers/conventions.md":"# Coding standards & readability reviewer\n\n**Scope:** all changed files (`**/*`).\n\nAdopt the reviewer persona and return findings in the output contract defined in\n`_contract.md`.\n\nReview the diff against **`.claude/rules/conventions.md`** (read it; it points to\n`CODING_STANDARDS.md`, the code-guidelines source of truth). Report each violation with\n`file:line`, severity:\n\n- **suggestion** — readability problems from the rule: unclear or misleading names, unnecessary\n  abstraction (indirection that earns no readability or reuse), single-line indirection, `?.` where\n  the null case isn't handled right there, needless whitespace/diff churn, or missing comments on\n  genuinely complex flow.\n- **suggestion** — any breach of the rule's **Reuse before you write** or **Replacing code leaves\n  nothing behind** sections; confirm what the new code actually renders or reads before calling a\n  leftover dead. From **Before you call it done**, report only what the diff itself shows: a new\n  branch or acceptance criterion with no test, or a changed return/payload shape whose assertions\n  elsewhere weren't updated. Whether the author ran the suite isn't visible here — don't guess at it.\n- **nit** — pure style/formatting/naming deviations (indent, quotes, semicolons, trailing commas,\n  arrow parens, casing) — most are auto-fixed by ESLint, so keep these brief.\n",".claude/skills/code-review/reviewers/correctness.md":"# Correctness & root-cause reviewer\n\n**Scope:** all changed source (`**/*`, excluding `tests/**`).\n\nAdopt the reviewer persona and return findings in the output contract defined in\n`_contract.md`.\n\nValidate that the change is correct and, for every bug fix, that it addresses the\nunderlying problem rather than the visible symptom. Surface-level patches that mask a\ndefect without resolving it are a **blocker**:\n\n- Understand *why* the issue exists before accepting the fix — trace the bug to its origin,\n  don't stop at where it surfaced.\n- Flag patches that suppress a symptom (extra null guards, try/catch swallowing, defensive\n  re-checks, retries, timeouts, clamping values) while leaving the real cause in place. Ask:\n  if this defends against a bad value, *where does the bad value come from*, and should it be\n  fixed there instead?\n- A fix at the wrong layer (UI guard for a data-layer bug, caller working around a callee's\n  contract violation) is a symptom patch — call out the correct layer.\n- Watch for fixes scoped to one reproduction case when the same root cause can manifest\n  elsewhere; the correct fix usually covers all call sites, not just the reported one.\n- When a fix looks like a workaround, say so and name the deeper change that would actually\n  resolve it — even if the larger fix is out of scope, the PR should acknowledge it.\n- Beyond bug fixes, check ordinary correctness: off-by-one and boundary errors, unhandled\n  promise rejections / missing `await`, swallowed errors, incorrect null/undefined handling,\n  and edge cases the change introduces.\n- **Check the twin path.** Bruno keeps parallel implementations of the same behavior — `.bru`\n  vs `.yml` serializers, the default app-data workspace vs custom-filesystem workspaces, the\n  Electron request pipeline vs its `bruno-cli` copy, the scripting API vs its QuickJS shim,\n  `bruno-filestore` vs the `bruno-converters` export. A change that touches one path must be\n  verified against its twin; behavior that diverges between them (a value that persists or\n  defaults in one but is dropped or defaulted differently in the other) is a bug, not two\n  independent features. The full map of which surfaces pair up is\n  `.claude/rules/feature-parity.md`; the `feature-parity` lens reviews it in depth, so raise\n  only the correctness consequence here and leave the enumeration to that lens.\n- **`x || default` on a field whose absence is meaningful.** When \"not set\" / \"never\n  configured\" is a distinct state, falsy-coalescing (`version || '1'`) fabricates a value for\n  the unset case, erases the distinction, and often diverges from a sibling path that handles\n  it correctly. Flag it; use `??` or an explicit `undefined` check when the unset state must\n  survive.\n",".claude/skills/code-review/reviewers/cross-platform.md":"# Cross-platform reviewer\n\n**Scope:** all changed files (`**/*`).\n\nAdopt the reviewer persona and return findings in the output contract defined in\n`_contract.md`.\n\nReview the diff against **`.claude/rules/cross-platform.md`** (read it) — Bruno ships on macOS,\nWindows, and Linux. Report each violation with `file:line`, severity:\n\n- **blocker** — anything that breaks a platform: hardcoded `/` or `\\` separators instead of\n  `path.join`/`path.resolve`; hardcoded paths (`/home/`, `C:\\Users\\`, `~/`) instead of\n  `os.homedir()`/`app.getPath()`; platform-specific shell/`child_process` without a fallback\n  (`which` vs `where`, `spawn` needing `shell:true` on Windows); Unix-only signals; `/tmp`\n  instead of `os.tmpdir()`.\n- **suggestion** — portable-but-fragile patterns not yet a concrete break: case-sensitivity\n  assumptions, inconsistent CRLF/LF handling, `fs.chmod`/`fs.access` assuming Unix permission bits.\n- **suggestion** — multiline `.bru`/text-block parsing that splits on `\\n` instead of a\n  CRLF-aware regex (`/\\r\\n|\\r|\\n/`) — see the rule's Line Endings section.\n",".claude/skills/code-review/reviewers/dsl-changes.md":"# On-disk DSL & serialization reviewer\n\n**Scope:** `packages/bruno-app/**`, `packages/bruno-electron/**`, `packages/bruno-cli/**`,\n`packages/bruno-lang/**`, `packages/bruno-filestore/**`, `packages/bruno-schema/**`,\n`packages/bruno-schema-types/**`, `packages/bruno-converters/**`.\nThe format packages own serialization, but DSL objects are assembled in `bruno-app` (Redux)\nand `bruno-electron` (IPC), and `bruno-cli` reads/writes these files directly — a shape change\nmade there won't be caught by the serialization layer, so they're all in scope.\n\nAdopt the reviewer persona and return findings in the output contract defined in\n`_contract.md`.\n\nReview the diff against **`.claude/rules/dsl-changes.md`** (read it) — Bruno persists\ncollections/requests/environments/config as `.bru` and `.yml` files read across app versions, so\nbackward compatibility is the priority. Report violations with `file:line`, severity:\n\n- **blocker** — a breaking field change (rename, remove, retype, repurpose, or changed\n  default/semantics); a new field that isn't optional with a safe default; format drift between\n  `bru` and `yml` (or between parse and stringify); a lossy round-trip (`stringify` drops\n  unknown fields, or `parse(stringify(x)) !== x`); a shape change with no read-time compat shim;\n  a cryptic, abbreviated, or inconsistent property name (names are permanent and user-facing).\n- **blocker** — new `.bru` *syntax* (escape form, block delimiter, annotation grammar) that older\n  parsers can't read; an escaping/delimiter change not proven by on-disk reparse\n  (`parse(read(serialize(x)))`) or that escapes a multi-char delimiter by splitting on the whole\n  delimiter. See the rule's escaping and forward-compat sections.\n- **suggestion** — missing round-trip test in both formats or missing old-format golden fixture;\n  a name/structure that won't scale (positional vs keyed, widening union).\n- Also push back on any **unnecessary** DSL change that could be solved in memory / UI without\n  altering the serialized shape.\n- **Not a finding:** the read-accepts-`{content}` / write-emits-`string` asymmetry on\n  `description` fields — string-only internally is an established convention (see rule), not a\n  lossy round-trip.\n",".claude/skills/code-review/reviewers/e2e-tests.md":"# E2E tests reviewer\n\n**Scope:** files under `tests/**` (`path: 'tests/**/**.*'`).\n\nAdopt the reviewer persona and return findings in the output contract defined in\n`_contract.md`.\n\nReview the diff against **`.claude/rules/testing.md`** and the e2e checklist in\n**`.coderabbit.yaml`** (read both). Report violations with `file:line`, severity:\n\n- **blocker** — `test.only`; `page.pause()`.\n- **suggestion** — `page.waitForTimeout()` where an `expect()` locator assertion could wait\n  instead; inline raw selectors instead of `tests/utils/page/*` modules; a spec that mutates a\n  committed fixture without restoring it in `afterAll`; shared state / non-isolated tmp paths.\n- **suggestion** — a non-discriminating assertion: `toContain('description:')` when the fixture\n  already carries that substring on unrelated rows passes even if the change under test never\n  happened; assert the unique value actually written by this change.\n- **suggestion** — parallel `.bru` and `.yml` fixtures for the same scenario that disagree\n  field-for-field (e.g. a row `selected: false` in one, enabled in the other) — a fixture bug\n  even when the current assertion tolerates it.\n- **nit** — locators not stored in variables; a single broad assertion where several focused\n  ones fit; steps not wrapped in `test.step`.\n",".claude/skills/code-review/reviewers/feature-parity.md":"# Co-related features & parity reviewer\n\n**Scope:** `packages/**`.\n\nAdopt the reviewer persona and return findings in the output contract defined in\n`_contract.md`.\n\nReview the diff against **`.claude/rules/feature-parity.md`** (read it). This lens asks one\nquestion of every changed file: **what else had to change with it, and did it?** For each changed\nfile, find it in the rule's tables, open the counterpart, and check whether the counterpart logic\nis in the diff. Report with `file:line`, severity:\n\n- **blocker** — logic changed on one side of an app ↔ CLI pair (§1) when the twin file holds the\n  counterpart logic and the diff leaves it untouched. Includes bug fixes in `bruno-electron` whose\n  root cause `bruno-cli` also has a copy of. Name the twin path and function.\n- **blocker** — a scripting API added or changed in `bruno-js/src/{bru,bruno-request,bruno-response}.js`\n  without the matching bridge in `bruno-js/src/sandbox/quickjs/shims/`.\n- **blocker** — a persisted field that skips a §2 layer: missing from `bruno-schema` (throws on\n  save), the app save transform, one on-disk format, the BRU → YML migration, or OpenCollection\n  import/export (either direction). Flag the missing surface only — the on-disk contract itself\n  (compat, escaping, round-trip) belongs to the `dsl-changes` lens; don't duplicate its findings.\n- **suggestion** — identical logic patched in both runtimes that has no Electron- or CLI-only\n  dependency and could move into `@usebruno/requests` / `@usebruno/common` (`utils/cookies.js` is\n  the precedent); otherwise, a missing comment naming the pairing at both sites.\n- **suggestion** — a new app runner option with no `bru run` flag; a feature at one scope level\n  with no decision for the others or `inherit`; a wire-request change without a\n  `snippet-generator.js` update; an HTTP-only change on a path gRPC/WebSocket also flow through;\n  a Postman/Insomnia/OpenAPI converter not updated for a field its format can represent.\n- **suggestion** — tests on one side of a pair only.\n\n**Not a finding:** a stated asymmetry, or one the rule lists as known-divergent (no preferences\nstore / Electron window / system-browser OAuth / Yup validation in the CLI; CLI runner handles\nonly `http-request` and `graphql-request`). A change to runtime-specific code in a paired file\n(e.g. an Electron-only helper in `utils/filesystem.js`) is not a gap. Never claim a missing twin\nwithout confirming the twin file exists and holds the counterpart logic.\n",".claude/skills/code-review/reviewers/react.md":"# React — app files reviewer\n\n**Scope:** `packages/bruno-app/**`.\n\nAdopt the reviewer persona and return findings in the output contract defined in\n`_contract.md`.\n\nReview changed components against **`CODING_STANDARDS.md` §React** (read it); for changes that\ntouch the store, also consult **`.claude/rules/redux-store.md`**. Report violations with\n`file:line`, severity:\n\n- **blocker** — an `useEffect` that could be derived state, an event handler, or a custom hook;\n  a hardcoded hex/rgb/hsl/named color instead of the styled-components `theme` prop (breaks the other\n  12 themes — verify the token path exists on the theme object); namespaced hook import (`React.useX`);\n  a component that mixes controlled and uncontrolled state; a hook called after a conditional\n  early return.\n- **blocker** — anything breaking **`.claude/rules/bruno-app-layout.md`** (read it),\n  component-specific vs shared placement included; the drift noted at the end of that file is not a\n  finding.\n- **blocker** — a new theme token defined in `themes/light|dark/*.js` without the matching property\n  added to `themes/schema/oss.js`, or added to the schema but missing from some of the 13 theme files.\n  The schema is `additionalProperties: false` and `providers/Theme/index.js` validates against it at\n  runtime, so either gap invalidates that whole theme and silently falls the user back to the default\n  with an error toast.\n- **suggestion** — a missing memo that breaks a dependency array or re-renders a heavy child, or\n  a gratuitous memo wrapping a cheap primitive; Tailwind used for colors (layout only);\n  a testable element without `data-testid`; a theme-token swap resolving to a different value than\n  the literal it replaced, or one value left hardcoded while its neighbours were themed (resolve it\n  in `themes/` first); a `document`/`window` listener not gated on the state that makes it relevant,\n  even where an incidental detail makes it harmless today.\n- **suggestion** — a new `EditableTable` column that omits the `readOnly`/`editMode` gating its\n  sibling columns carry (`readOnly={column.readOnly}` at render): the field stays editable in\n  view / read-only mode even though the change handler discards the edit.\n- **suggestion** — optimistic success state (`copied`, `saved`, `done`) set unconditionally\n  rather than gated on the operation resolving — e.g. `copied = true` after an optional-chained\n  `navigator.clipboard?.writeText` that no-ops in an insecure context — showing a false\n  confirmation.\n",".claude/skills/code-review/reviewers/security.md":"# Security & data safety reviewer\n\n**Scope:** all changed source (`**/*`, excluding `tests/**`).\n\nAdopt the reviewer persona and return findings in the output contract defined in\n`_contract.md`.\n\nBruno is an offline API client that handles user credentials, tokens, and runs\nuser-authored scripts in a sandbox. Review changes for these Bruno-specific risks:\n\n- **No secret leakage.** Auth tokens, passwords, API keys, OAuth2 secrets, and env-var\n  values must never be written to logs, console, error messages, or telemetry. A secret\n  logged in cleartext is a **blocker**. Watch for `console.log`/logger calls that dump a\n  whole request, header set, or config object containing credentials.\n- **Script sandbox integrity** (`bruno-js`, QuickJS / Node VM). Changes that widen the\n  sandbox surface — exposing new Node built-ins, `require`, filesystem, or `process` to\n  user scripts, or passing unsanitized script output back into privileged code — are a\n  potential sandbox escape; call them out.\n- **IPC input validation** (`bruno-electron` handlers, `preload.js`). Treat every argument\n  arriving from the renderer as untrusted: validate file paths (guard against traversal\n  outside the collection dir), types, and bounds before acting on them.\n- **Path traversal & arbitrary writes.** File reads/writes derived from user or request\n  data must be confined to the intended directory — no `../` escape, no writing outside the\n  collection root.\n- **Injection & unsafe eval.** Flag string-built shell commands, `eval`, or dynamic\n  `Function` on untrusted input, and unescaped interpolation into commands or queries.\n- **Dependency & network surface.** Note new runtime dependencies or outbound network calls\n  that don't fit Bruno's offline-first, privacy-first posture.\n\nKeep findings concrete — tie each to how the tainted value reaches the sink.\n",".claude/skills/write-e2e-test/SKILL.md":"---\nname: write-e2e-test\ndescription: Write a Playwright E2E test for Bruno. Use when adding or modifying\n  tests under tests/, creating a new test suite, or reproducing a bug as an\n  e2e spec.\n---\n\n# Writing a Bruno E2E test\n\nRead `.claude/rules/testing.md` first for the full fixture and helper reference. See\n`docs/playwright-testing-guide.md` for the extended walkthrough (codegen, running,\ndebugging, troubleshooting) — but where it disagrees with this skill or the actual repo,\ntrust the repo: specs live in `tests/` (not `e2e-tests/`), the suite is `fullyParallel`,\nand projects are `default`, `system-pac`, `auth`, and `ssl`\n(verify in `playwright.config.ts`).\n\n## Start with codegen or by hand\n\n- **Codegen** (fastest for a new flow): `npm run test:codegen <name>` launches the app,\n  records your clicks, and writes `tests/<name>.spec.ts`. Treat the output as a rough\n  draft — move it into the right area folder, swap generated selectors for `data-testid`\n  or helpers, wrap steps in `test.step`, and delete dead recorded actions.\n- **By hand**: `import { test, expect } from '../../playwright'` and follow the steps below.\n\n## Steps\n\n1. **Place the spec** at `tests/<area>/<name>.spec.ts` — reuse an existing area dir under\n   `tests/` (list it for the current set; e.g. `request/`, `collection/`, `environments/`,\n   `runner/`, `scripting/`) or add one. Pick the project: `default` for most specs;\n   `auth` / `ssl` / `system-pac` only when the test needs those\n   servers or external dependencies.\n2. **Use fixtures** from `playwright/index.ts` (read it for the full set) instead of launching\n   the app yourself:\n   - `page` / `electronApp` — default app with onboarding skipped.\n   - `pageWithUserData` — seeds data from `tests/<area>/init-user-data/`\n     (use `init-user-data-fresh/` for new-user flows).\n   - `createTmpDir(tag?)` — throwaway dir for collections/files the test creates; cleaned\n     up automatically, so prefer it over hardcoded paths.\n   - `newPage`, `launchElectronApp`, `reuseOrLaunchElectronApp`, `restartApp` — for\n     multi-window or app-restart scenarios.\n   - `collectionFixturePath` / `workspaceFixturePath` — copy a committed fixture to a tmp\n     dir and get its path.\n3. **Use helpers** from `tests/utils/page/` instead of hand-rolling clicks — read the module\n   for the current set (e.g. `createCollection`, `sendRequest`, `runCollection`).\n4. **Structure each test** arrange → act → assert, and wrap the phases in `test.step` so\n   the HTML report reads clearly. Prefer several focused assertions over one broad check.\n5. **Wait for readiness**: `await page.locator('[data-app-state=\"loaded\"]').waitFor()`\n   before asserting on app state.\n6. **Locate** by `data-testid` (Bruno's dominant convention) or documented classes\n   (`.collection-item-name`); stable `getByRole` / `getByText` locators are fine too.\n   Avoid brittle CSS, `#id`, or index-based selectors. If no stable selector exists, add a\n   `data-testid` to the component rather than locating by text.\n7. **If the test mutates a committed fixture**, restore it in `afterAll`. Existing specs\n   do this with `git checkout <fixturePath>` (e.g. `tests/response-examples/*.spec.ts`) —\n   follow the pattern already used by neighbouring specs in that area.\n8. **Use auto-retrying assertions** (`expect(locator).toBeVisible()`); never assert\n   immediately after a file-watcher-triggered change (created files take a tick to appear).\n   Reserve `page.waitForTimeout()` for when no locator assertion can wait instead.\n9. **Run it**: `npx playwright test tests/<area>/<name>.spec.ts --project=default`\n   (the web + test servers start automatically). Debug with `--ui`, `--headed`, `--debug`,\n   or `--trace on` (then `npx playwright show-trace test-results/**/trace.zip`).\n\n## Checklist before done\n\n- [ ] Test is isolated — unique tmp paths (`createTmpDir`), no state shared with other tests\n- [ ] Passes on a clean second run (retry-safe; worker fixtures persist across retries)\n- [ ] Folders expanded before asserting on their children (collapsed = not in DOM)\n- [ ] Any mutated committed fixture is restored in `afterAll`\n- [ ] Any codegen-generated draft refactored to fixtures/helpers + stable selectors\n- [ ] Steps wrapped in `test.step`; no `test.only`, no `page.pause()`\n","packages/bruno-app/storybook/README.md":"# Storybook\n\nStorybook is our workbench and living documentation for the `bruno-app` UI — primarily the design-system primitives under `src/ui`. Use it to build a component in isolation, see every state at once, and check how it looks across Bruno's themes.\n\n## Running\n\nFrom the repo root:\n\n```bash\nnpm run storybook --workspace=packages/bruno-app        # dev server on http://localhost:6006\nnpm run build-storybook --workspace=packages/bruno-app  # static build -> storybook-static/\n```\n\nConfig lives in this folder: `main.js` (framework, addons, stories glob, webpack aliases) and `preview.jsx` (global decorators, the theme toolbar).\n\n## Where stories live\n\nStories are co-located next to the component they document and picked up by the glob in `main.js`:\n\n```text\nsrc/**/*.stories.@(js|jsx|mjs|ts|tsx)\n```\n\nSo `src/ui/Button/index.js` is documented by `src/ui/Button/Button.stories.jsx`. `main.js` mirrors the app's path aliases (`ui`, `components`, `themes`, `providers`, `utils`, …), so imports like `import Button from 'ui/Button'` work inside stories.\n\n## Writing a story\n\nStandard CSF. Give the component a `Components/<Name>` title and tag it `autodocs` so it gets a Docs page:\n\n```jsx\nimport Button from './index';\n\nexport default {\n  title: 'Components/Button',\n  component: Button,\n  parameters: { layout: 'centered' },\n  tags: ['autodocs'],\n  argTypes: {\n    variant: { control: 'select', options: ['filled', 'outline', 'ghost'] }\n  }\n};\n\nexport const Default = { args: { children: 'Button' } };\n```\n\nTheming is automatic — `preview.jsx` wraps every story in the styled-components `ThemeProvider` (and the app's `ThemeContext`), so components read `props.theme` and `useTheme()` normally. Don't add your own theme provider.\n\n## Theme switching\n\nThe toolbar has a **Theme** picker listing all of Bruno's registered themes (from `themes/index.js` `themeRegistry`). Selecting one re-renders the current story with that theme's real tokens and sets the preview's light/dark mode.\n\nNote: components styled via styled-components `props.theme` (nearly all of `src/ui`) get full per-theme accuracy. Anything relying on raw `var(--color-*)` CSS variables only toggles light vs dark, since those variables aren't per-theme.\n\n## Conventions & gotchas\n\nThese are worth following so components render correctly on the **Docs** page, where every story renders inline in one shared document (unlike Canvas, which isolates each story in an iframe).\n\n**Keep the Docs description clean.** Storybook uses a component's leading JSDoc as the Docs description and does not strip `@param` tags, so a rich JSDoc block dumps as a run-on paragraph. Set an explicit description in the story instead of trimming the component's source comments:\n\n```js\nparameters: {\n  docs: { description: { component: 'One clean paragraph. See the props table below.' } }\n}\n```\n\n**Don't hardcode `name` on radio-based groups.** `RadioGroup`/`SegmentedControl` auto-generate a unique `name` per instance. On the Docs page the primary story renders twice and all stories share the DOM, so a hardcoded `name` makes separate instances collide into one native radio group (selecting in one unchecks another). Omit `name` unless you specifically want a shared native group.\n\n**Give overlay components room.** Dropdowns, menus, and modals open a popover that gets clipped by the short, `overflow`-hidden inline Docs preview. Render those stories in an iframe with a height:\n\n```js\nparameters: { docs: { story: { inline: false, height: '360px' } } }\n```\n\n**Always give group components an accessible name.** Components like `RadioGroup`/`SegmentedControl` warn in development if they render without `label`, `ariaLabel`, or `ariaLabelledBy` — pass one in stories and real usage. Don't fabricate a placeholder name to silence the warning; a meaningless label is worse than none (only the shared native `name` attribute is auto-generated, because it carries no human meaning).\n\n**Inline story state — don't wrap in a helper component.** For controlled components, put `useState` directly inside the story's `render` and return the component itself. Wrapping it in a `<Controlled>` helper makes the code panel show `<Controlled>` instead of the real component, because the generated source reflects the root element the `render` returns.\n\n```jsx\nexport const Default = {\n  render: (args) => {\n    const [value, setValue] = useState('http');\n    return <SegmentedControl {...args} value={value} onChange={setValue} items={items} />;\n  }\n};\n```\n\n**Demo wrappers, not component chrome.** If a story needs a sized/bordered container to demonstrate behavior (e.g. width for overflow), keep it clearly a demo wrapper so it isn't mistaken for the component's own styling.\n\n## Docs addon\n\nAutodocs (the Docs tab) requires `@storybook/addon-docs`, registered in `main.js` and pinned to the **same version** as the `storybook` core package — a version mismatch throws `docsParameter.renderer is not a function`. Run `npx storybook doctor` to check for mismatched Storybook packages.\n\n## Code panel\n\n`preview.jsx` turns on the source/code views that `@storybook/addon-docs` provides:\n\n```js\ndocs: {\n  codePanel: true,              // a \"Code\" tab in the Canvas addon panel\n  canvas: { sourceState: 'shown' } // Docs \"Show code\" expanded by default\n}\n```\n\nThe snippet reflects the root element your story's `render` returns, so follow the \"inline story state\" note above to keep it showing the real component. There's also a `</>` Show code button in the Canvas toolbar.\n\n## Adding a new primitive\n\n1. Build the component under `src/ui/<Name>/` (`index.js` + `StyledWrapper.js`), following the existing conventions: `$`-prefixed transient style props, theme tokens, a `dataTestId`, forwarded refs, and forwarded `...rest`.\n2. Add `<Name>.stories.jsx` next to it with a `Components/<Name>` title, `autodocs`, `argTypes`, a clean `docs.description.component`, and a story per meaningful state.\n3. Add `<Name>.spec.jsx` (React Testing Library) for behavior and accessibility.\n\n### Group primitives (single-select)\n\nSingle-select group primitives (`RadioGroup`, `SegmentedControl`) follow a specific layout: the implementation lives in a `components/` subfolder and only the data-driven **list** API is exposed publicly, keeping the compound pieces internal so a compound API can be surfaced later without a rewrite.\n\n```text\nsrc/ui/<Name>/\n├── components/\n│   ├── <Name>Base.jsx    # compound: owns the context + renders the radiogroup\n│   ├── <Option>.jsx      # the internal option (reads context)\n│   ├── <Name>.jsx        # public list component — maps `items` → options\n│   └── StyledWrapper.js  # styling\n├── index.jsx             # exposes only the list <Name> (default export)\n├── <Name>.stories.jsx\n└── <Name>.spec.jsx\n```\n\nConsumers use the list API: `<SegmentedControl value onChange items={[{ value, label, ... }]} />`. To expose the compound API later, re-export `<Option>` / `<Name>Base` from `index.jsx`.\n","packages/bruno-cli/docker/README.md":"# Bruno CLI Docker Images\n\nOfficial Docker images for [Bruno CLI](https://www.usebruno.com), enabling container-native API collection runs in CI/CD pipelines and local environments without requiring Node.js or npm on the host. See the [Bruno CLI docs](https://docs.usebruno.com/bru-cli/overview) for CLI usage.\n\n## Registries\n\n```bash\ndocker pull usebruno/cli:latest\ndocker pull ghcr.io/usebruno/cli:latest\n```\n\n---\n\n## Variants\n\n| Variant | Base image | Details |\n|---------|-----------|---------|\n| **Alpine** (default) | `node:22-alpine` | [→ Alpine README](https://github.com/usebruno/bruno/blob/main/packages/bruno-cli/docker/images/alpine/README.md) |\n| **Debian** | `node:22-slim` | [→ Debian README](https://github.com/usebruno/bruno/blob/main/packages/bruno-cli/docker/images/debian/README.md) |\n\n### Quick choice\n\n- **Use Alpine** unless you have a specific reason not to (90% of users)\n- **Use Debian** if you hit SSL/glibc compatibility issues\n\n---\n\n## Tags\n\nEvery release publishes the following tags to **both** Docker Hub (`usebruno/cli`) and GHCR (`ghcr.io/usebruno/cli`).\n\n### Alpine variant (default)\n\n| Tag pattern | Example | Notes |\n|-------------|---------|-------|\n| `latest` | `usebruno/cli:latest` | Newest release marked as latest. Only moves when the publish workflow is run with \"Tag this version as latest\" checked. |\n| `latest-alpine` | `usebruno/cli:latest-alpine` | Alias of `latest` — also alpine. |\n| `alpine` | `usebruno/cli:alpine` | Newest alpine, moves on every alpine publish. |\n| `<version>` | `usebruno/cli:3.3.0` | Exact version, immutable. |\n| `<version>-alpine` | `usebruno/cli:3.3.0-alpine` | Exact version, explicitly alpine. |\n| `<major.minor>` | `usebruno/cli:3.3` | Floats with patch releases (3.3.x). |\n| `<major.minor>-alpine` | `usebruno/cli:3.3-alpine` | Same, explicitly alpine. |\n| `<major>` | `usebruno/cli:3` | Floats with any 3.x.x release. |\n| `<major>-alpine` | `usebruno/cli:3-alpine` | Same, explicitly alpine. |\n\n### Debian variant\n\n| Tag pattern | Example | Notes |\n|-------------|---------|-------|\n| `latest-debian` | `usebruno/cli:latest-debian` | Newest debian release marked as latest (gated by the same checkbox). |\n| `debian` | `usebruno/cli:debian` | Newest debian, moves on every debian publish. |\n| `<version>-debian` | `usebruno/cli:3.3.0-debian` | Exact version, debian. |\n| `<major.minor>-debian` | `usebruno/cli:3.3-debian` | Floats with debian patch releases. |\n| `<major>-debian` | `usebruno/cli:3-debian` | Floats with any 3.x.x debian release. |\n\nThe unsuffixed tags (`:latest`, `:3.3.0`, `:3.3`, `:3`, `:alpine`) always resolve to the alpine variant by convention.\n\n---\n\n## Step-by-step guide\n\n### Step 1 — Pull the image\n\n```bash\n# latest (alpine by default — smallest, fastest to pull)\ndocker pull usebruno/cli:latest\n\n# specific version (recommended for production CI)\ndocker pull usebruno/cli:3.3.0\n\n# major.minor — gets patch updates automatically\ndocker pull usebruno/cli:3.3\n\n# debian variant\ndocker pull usebruno/cli:debian\ndocker pull usebruno/cli:3.3.0-debian\n```\n\n---\n\n#### Check it works\n\n```bash\ndocker run --rm usebruno/cli --version\n```\n\n---\n\n### Step 2 — Run your collection\n\n> These examples assume you are running `docker` from your Bruno collection directory. Mount that directory to `/bruno` and pass `bru` arguments directly after the image name. If your collection lives elsewhere on disk, see the path-based examples further down.\n\n> **Cross-platform note:** the examples below use `$(pwd)` which works in Bash / Zsh / Git Bash / WSL. On Windows native shells, substitute `$(pwd)` with `${PWD}` (PowerShell) or `%cd%` (CMD).\n\n> **Adding `bru` options:** The examples below show docker-specific flags. For all `bru run` options — recurse (`-r`), environments, variables, reporters, bail, and more — see the [Bruno CLI docs](https://docs.usebruno.com/bru-cli/overview).\n\n```bash\n# run every request in the Bruno collection (current dir)\ndocker run -v $(pwd):/bruno usebruno/cli run \n\n# run a specific subfolder (group of requests) within that collection\ndocker run -v $(pwd):/bruno usebruno/cli run ./api-tests\n\n# run a single .bru request file from that collection\ndocker run -v $(pwd):/bruno usebruno/cli run ./api-tests/login.bru\n\n# write a JUnit XML report (lands in the current directory because of the bind mount)\ndocker run -v $(pwd):/bruno usebruno/cli run --reporter-junit results.xml\n```\n\nFor Windows CMD users, swap `$(pwd)` with `%cd%`:\n\n```cmd\ndocker run -v %cd%:/bruno usebruno/cli run \n```\n\n#### Running a collection that lives at a different path\n\nIf your collection is not in your current directory, point `docker` at its path (relative or absolute) instead of `$(pwd)`:\n\n```bash\n# run every request in a collection at an arbitrary path\ndocker run -v /path/to/your/collection:/bruno usebruno/cli run\n\n# run a single .bru file from a collection at an arbitrary path\ndocker run -v /path/to/your/collection:/bruno usebruno/cli run ./auth/login.bru\n```\n\n> **Note on `--rm`:** Examples below include `--rm`. Docker keeps stopped containers around after they exit, which lets you `docker logs` or `docker inspect` them later for debugging. If you'd rather have Docker auto-delete the container as soon as `bru` finishes — useful for CI runs or to avoid `docker ps -a` filling up with stale entries — append `--rm` to any `docker run` (or `docker compose run`) command:\n\n```bash\ndocker run --rm -v $(pwd):/bruno usebruno/cli run\n```\n\n> It's purely a cleanup convenience; it doesn't affect the image, mounts, stdout output, or exit code.\n\n\n## CI/CD integration\n\n### GitHub Actions\n\n```yaml\njobs:\n  api-tests:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n\n      - name: Run Bruno collection\n        run: |\n          docker run --rm \\\n            -v ${{ github.workspace }}:/bruno \\\n            usebruno/cli:latest run --output results.xml --format junit\n\n      - name: Publish Test Report\n        uses: dorny/test-reporter@v3\n        if: success() || failure()\n        with:\n          name: Bruno Test Results\n          path: ${{github.workspace}}/results.xml\n          reporter: java-junit\n```\n\n### GitLab CI\n\n```yaml\napi-tests:\n  image: usebruno/cli:latest\n  script:\n    - bru run --output results.xml --format junit\n  artifacts:\n    reports:\n      junit: results.xml\n```\n\n---\n\n## Docker Compose\n\n### Quick example\n\nA minimal `docker-compose.yml` for running a Bruno collection alongside your project:\n\n```yaml\nservices:\n  bruno-cli:\n    image: usebruno/cli:latest\n    container_name: bruno-cli-runner\n    volumes:\n      - /path/to/collection:/bruno\n      - /path/to/reports:/reports\n    command:\n      run .\n      -r\n      --env ci\n      --reporter-json /reports/results.json\n      --reporter-junit /reports/results.xml\n      --reporter-html /reports/results.html\n```\n\nThen run:\n\n```bash\ndocker compose run bruno-cli\n```\n\nThe `/path/to/reports:/reports` mount catches the JSON, JUnit XML, and HTML reports on the host — drop any `--reporter-*` flag to skip that format.\n\n### Try it from this repo\n\nA ready-to-run `docker-compose.yml` lives in this repo at [`packages/bruno-tests/docker-compose.yml`](https://github.com/usebruno/bruno/blob/main/packages/bruno-tests/docker-compose.yml). It mounts the sibling `collection/` directory into the container, runs the `echo` folder against the `Prod` environment, and writes JSON, JUnit XML, and HTML reports into `packages/bruno-tests/reports/`:\n\n```bash\ncd packages/bruno-tests\ndocker compose run bruno-cli\n```\n\nThis fires a small set of requests against public endpoints that demonstrate the CLI executing requests and assertions inside a container.\n\n### Standalone demo\n\nFor a clone-and-run demo with a curated collection, see [`bruno-collections/bruno-cli-docker`](https://github.com/bruno-collections/bruno-cli-docker).\n\n---\n\n## Image details\n\nAll variants include:\n\n- **Entrypoint:** `bru`\n- **Working directory:** `/bruno`\n- **User:** `node` (UID 1000, non-root)\n- **Architectures:** `linux/amd64`, `linux/arm64`\n\n","packages/bruno-cli/docker/images/alpine/README.md":"# Bruno CLI — Alpine\n\nAlpine Linux variant of the Bruno CLI Docker image.\n\n**Base image:** `node:22-alpine`\n\n## Building\n\n```bash\ndocker build -t usebruno/cli:alpine ./images/alpine\n\n# with specific Bruno CLI version\ndocker build \\\n  --build-arg BRUNO_VERSION=3.3.0 \\\n  -t usebruno/cli:3.3.0-alpine \\\n  ./images/alpine\n```\n\n## Usage\n\n```bash\n# Run a collection\ndocker run -v $(pwd):/bruno usebruno/cli:alpine run\n\n# with pinned version\ndocker run -v $(pwd):/bruno usebruno/cli:3.3.0-alpine run\n```\n","packages/bruno-cli/docker/images/debian/README.md":"# Bruno CLI — Debian\n\nDebian slim variant of the Bruno CLI Docker image.\n\n**Base image:** `node:22-slim`\n\n## Building\n\n```bash\ndocker build -t usebruno/cli:debian ./images/debian\n\n# with specific Bruno CLI version\ndocker build \\\n  --build-arg BRUNO_VERSION=3.3.0 \\\n  -t usebruno/cli:3.3.0-debian \\\n  ./images/debian\n```\n\n## Usage\n\n```bash\n# Run a collection\ndocker run -v $(pwd):/bruno usebruno/cli:debian run \n\n# with pinned version\ndocker run -v $(pwd):/bruno usebruno/cli:3.3.0-debian run \n```\n","packages/bruno-cli/readme.md":"# Bruno CLI\n\nWith Bruno CLI, you can now run your API collections with ease using simple command line commands.\n\nThis makes it easier to test your APIs in different environments, automate your testing process, and integrate your API tests with your continuous integration and deployment workflows.\n\nFor detailed documentation, visit [Bruno CLI Documentation](https://docs.usebruno.com/bru-cli/overview).\n\n## Installation\n\nTo install the Bruno CLI, use the node package manager of your choice, such as NPM:\n\n```bash\nnpm install -g @usebruno/cli\n```\n\n## Getting started\n\nNavigate to the directory where your API collection resides, and then run:\n\n```bash\nbru run\n```\n\nThis command will run all the requests in your collection. You can also run a single request by specifying its filename:\n\n```bash\nbru run request.bru\n```\n\nOr run all requests in a collection's subfolder:\n\n```bash\nbru run folder\n```\n\nIf you need to use an environment, you can specify it with the `--env` option:\n\n```bash\nbru run folder --env Local\n```\n\nIf you need to collect the results of your API tests, you can specify the `--output` option:\n\n```bash\nbru run folder --output results.json\n```\n\nIf you need to run a set of requests that connect to peers with both publicly and privately signed certificates respectively, you can add private CA certificates via the `--cacert` option. By default, these certificates will be used in addition to the default truststore:\n\n```bash\nbru run folder --cacert myCustomCA.pem\n```\n\nIf you need to limit the trusted CA to a specified set when validating the request peer, provide them via `--cacert` and in addition use `--ignore-truststore` to disable the default truststore:\n\n```bash\nbru run request.bru --cacert myCustomCA.pem --ignore-truststore\n```\n\n## Importing Collections\n\nYou can import collections from other formats, such as OpenAPI, using the import command:\n\n```bash\nbru import openapi --source api.yml --output ~/Desktop/my-collection --collection-name \"My API\"\n```\n\nYou can also use the shorter form with aliases:\n\n```bash\nbru import openapi -s api.yml -o ~/Desktop/my-collection -n \"My API\"\n```\n\nThis creates a Bruno collection directory that can be opened in Bruno.\n\nYou can also import directly from a URL:\n\n```bash\nbru import openapi --source https://example.com/api-spec.json --output ~/Desktop --collection-name \"Remote API\"\n```\n\nYou can also export the collection as a JSON file:\n\n```bash\nbru import openapi --source api.yml --output-file ~/Desktop/my-collection.json --collection-name \"My API\"\n```\n\nImport Options:\n\n| Option                    | Details                                            |\n| ------------------------- | -------------------------------------------------- |\n| --source, -s              | Path to the source file or URL (required)          |\n| --output, -o              | Path to the output directory                       |\n| --output-file, -f         | Path to the output JSON file                       |\n| --collection-name, -n     | Name for the imported collection                   |\n| --insecure                | Skip SSL certificate validation when fetching from URLs |\n\n## Command Line Options\n\n| Option                       | Details                                                                       |\n| ---------------------------- | ----------------------------------------------------------------------------- |\n| -h, --help                   | Show help                                                                     |\n| --version                    | Show version number                                                           |\n| -r                           | Indicates a recursive run (default: false)                                    |\n| --cacert [string]            | CA certificate to verify peer against                                         |\n| --env [string]               | Specify environment to run with                                               |\n| --env-var [string]           | Overwrite a single environment variable, multiple usages possible             |\n| -o, --output [string]        | Path to write file results to                                                 |\n| -f, --format [string]        | Format of the file results; available formats are \"json\" (default) or \"junit\" |\n| --reporter-json [string]     | Path to generate a JSON report                                                |\n| --reporter-junit [string]    | Path to generate a JUnit report                                               |\n| --reporter-html [string]     | Path to generate an HTML report                                               |\n| --insecure                   | Allow insecure server connections                                             |\n| --tests-only                 | Only run requests that have tests                                             |\n| --bail                       | Stop execution after a failure of a request, test, or assertion               |\n| --csv-file-path              | CSV file to run the collection with                                           |\n| --reporter--skip-all-headers | Skip all headers in the report                                                |\n| --reporter-skip-headers      | Skip specific headers in the report                                           |\n| --client-cert-config         | Client certificate configuration by passing a JSON file                       |\n| --delay [number]             | Add delay to each request                                                     |\n\n## Scripting\n\nBruno cli returns the following exit status codes:\n\n- `0` -- execution successful\n- `1` -- an assertion, test, or request in the executed collection failed\n- `2` -- the specified output directory does not exist\n- `3` -- the request chain seems to loop endlessly\n- `4` -- bru was called outside of a collection root directory\n- `5` -- the specified input file does not exist\n- `6` -- the specified environment does not exist\n- `7` -- the environment override was not a string or object\n- `8` -- an environment override is malformed\n- `9` -- an invalid output format was requested\n- `255` -- another error occurred\n\n## Demo\n\n![demo](assets/images/cli-demo.png)\n\n## Support\n\nIf you encounter any issues or have any feedback or suggestions, please raise them on our [GitHub repository](https://github.com/usebruno/bruno)\n\nThank you for using Bruno CLI!\n\n## Changelog\n\n<!-- An absolute link is used here because npm treats links differently -->\n\nSee [https://github.com/usebruno/bruno/releases](https://github.com/usebruno/bruno/releases)\n\n## License\n\n[MIT](license.md)\n","packages/bruno-common/readme.md":"# bruno-common\n\nA collection of common utilities used across Bruno App, Electron and CLI packages.\n\n### Publish to Npm Registry\n\n```bash\nnpm publish --access=public\n```\n","packages/bruno-converters/readme.md":"# bruno-converters\n\nThe converters package is responsible for converting collections from one format to a Bruno collection.\nIt can be used as a standalone package or as a part of the Bruno framework.\n\n## Installation\n\n```bash\nnpm install @usebruno/converters\n```\n\n## Usage\n\n### Convert Postman collection to Bruno collection\n\n```javascript\nconst { postmanToBruno } = require('@usebruno/converters');\n\n// Convert Postman collection to Bruno collection\nconst brunoCollection = postmanToBruno(postmanCollection);\n```\n\n### Convert Postman Environment to Bruno Environment\n\n```javascript\nconst { postmanToBrunoEnvironment } = require('@usebruno/converters');\n\nconst brunoEnvironment = postmanToBrunoEnvironment(postmanEnvironment);\n```\n\n### Convert Insomnia collection to Bruno collection\n\n```javascript\nconst { insomniaToBruno } = require('@usebruno/converters');\n\nconst brunoCollection = insomniaToBruno(insomniaCollection);\n```\n\n### Convert OpenAPI specification to Bruno collection\n\n```javascript\nconst { openApiToBruno } = require('@usebruno/converters');\n\nconst brunoCollection = openApiToBruno(openApiSpecification);\n```\n\n### Convert WSDL file to Bruno collection\n\n```javascript\nimport { wsdlToBruno } from '@usebruno/converters';\n\nconst brunoCollection = await wsdlToBruno(wsdlContent);\n```\n\n## Example \n\n```javascript\n\nconst { postmanToBruno } = require('@usebruno/converters');\nconst { writeCollectionToDisk } = require('@usebruno/filestore');\nconst fs = require('fs/promises');\n\nasync function convertPostmanToBruno(inputFile, outputDir) {\n  const inputData = await fs.readFile(inputFile, 'utf8');\n\n  const collection = await postmanToBruno(JSON.parse(inputData));\n  // Writes YAML (OpenCollection) files by default\n  await writeCollectionToDisk(collection, outputDir);\n  // Pass { format: 'bru' } to write .bru files instead\n  // await writeCollectionToDisk(collection, outputDir, { format: 'bru' });\n}\n\nconvertPostmanToBruno('demo_collection.postman_collection.json', './bruno-collection');\n\n``` \n\n## WSDL Import Features\n\nThe WSDL importer supports the following features:\n\n- **Service Discovery**: Automatically extracts service endpoints from WSDL definitions\n- **Operation Mapping**: Converts WSDL operations to Bruno HTTP requests\n- **SOAP Envelope Generation**: Creates proper SOAP envelopes for each operation\n- **Header Configuration**: Sets up appropriate Content-Type and SOAPAction headers\n- **Environment Variables**: Creates environment variables for service base URLs\n- **Folder Organization**: Groups operations by port type for better organization\n\n### WSDL Import Example\n\n```javascript\nimport { wsdlToBruno } from '@usebruno/converters';\nimport { writeCollectionToDisk } from '@usebruno/filestore';\nimport fs from 'fs/promises';\n\nasync function importWSDL() {\n  const wsdlContent = await fs.readFile('service.wsdl', 'utf8');\n\n  const collection = await wsdlToBruno(wsdlContent);\n  // Writes YAML (OpenCollection) files by default\n  await writeCollectionToDisk(collection, './soap-collection');\n  // Pass { format: 'bru' } to write .bru files instead\n  // await writeCollectionToDisk(collection, './soap-collection', { format: 'bru' });\n}\n\nimportWSDL();\n```\n\n### CLI Usage\n\nYou can also use the Bruno CLI to import WSDL files:\n\n```bash\n# Import WSDL file to a directory\nbruno import wsdl --source service.wsdl --output ~/Desktop/soap-collection --collection-name \"SOAP Service\"\n\n# Import WSDL from URL\nbruno import wsdl --source https://example.com/service.wsdl --output ~/Desktop --collection-name \"Remote SOAP Service\"\n\n# Import WSDL and save as JSON file\nbruno import wsdl --source service.wsdl --output-file ~/Desktop/soap-collection.json --collection-name \"SOAP Service\"\n```\n\n## Supported Formats\n\n- **Postman Collections** (v2.1)\n- **Insomnia Collections** (v4 and v5)\n- **OpenAPI Specifications** (v3.0)\n- **WSDL Files** (Web Services Description Language)\n\n## Dependencies\n\n- `lodash` - Utility functions\n- `nanoid` - UUID generation\n- `js-yaml` - YAML parsing\n- `xml2js` - XML parsing for WSDL\n- `@usebruno/schema` - Schema validation\n","packages/bruno-converters/tests/postman/round-trip/README.md":"# Round-trip conversion tests\n\nThis directory tests conversion **fidelity** by sending a real collection through both converters and\ncomparing the result against the input:\n\n```\nPostman export ──postmanToBruno──▶ Bruno collection ──brunoToPostman──▶ Postman export\n     (input)                                                              (output)\n        └──────────────────── compared, subtree by subtree ──────────────────┘\n```\n\nCurrently only the **auth** subtree is covered ([`auth.spec.js`](./auth.spec.js)), but the harness is\nbuilt so other subtrees (body, headers, scripts, params, …) can be added as sibling files with no new\nplumbing. See [Extending](#extending) below.\n\n---\n\n## Why round-trip, and why this direction\n\nThe per-converter unit specs (`process-auth`, `request-auth`, `bruno-to-postman`, …) each exercise\n**one** converter in isolation and assert a hand-written expected object. That has two blind spots:\n\n1. **Asymmetry between the two converters** — a field renamed on import (`tokenName` → `credentialsId`)\n   but not renamed back on export is invisible to a one-sided test. A round-trip is the only thing that\n   exercises both mappings against each other.\n2. **Circular expectations** — a hand-written \"expected\" object is only as correct as the author's\n   reading of the converter. If you misread it, the test encodes the misreading.\n\nWe focus on **Postman → Bruno → Postman** (not Bruno → Postman → Bruno) because that is the direction\nwhere a **real exported `.postman_collection.json`** is the input. Real exports carry the exact key\nnames, the full v2.1 array-backed auth format, and even fields Bruno doesn't model — ground truth that\na hand-built object can't reproduce.\n\n---\n\n## Why the fixture approach\n\nTests are **data-driven off the files in [`fixtures/`](./fixtures)**. Every `*.postman_collection.json`\nis discovered automatically and run through the round-trip. This is deliberate:\n\n- **No hand-written expected values.** The input fixture's own auth is the source of truth — the test\n  asserts the output equals the input (minus documented exceptions), so adding coverage is \"drop a\n  file\", not \"write an expectation\".\n- **Real-world shape.** Fixtures are actual Postman exports, so they include the quirks (omitted\n  defaults, extra keys, ordering) that synthetic objects miss.\n\n### Extending\n\n- **More auth coverage:** export a collection of the desired auth type from Postman, drop it into\n  `fixtures/`. It's picked up automatically. A new whitelist entry is only needed if it exercises a\n  field Bruno doesn't model yet (see below).\n- **Another subtree** (e.g. body): add `body.spec.js` alongside `auth.spec.js`, reusing the same\n  fixtures. Factor the shared \"walk + diff by node path\" logic the way [`compare-auth.js`](./compare-auth.js)\n  does for auth, keeping each subtree's normalization rules in its own module.\n\n---\n\n## The whitelist and its categories\n\nComparison is **strict**: any difference in the auth subtree fails the test. Differences that are\nexpected today are recorded in `AUTH_ROUNDTRIP_WHITELIST` in [`auth.spec.js`](./auth.spec.js), each\ntagged with a `category`. The category is the whole point — it separates \"bug we haven't fixed\" from\n\"working as intended\":\n\n| Category | Meaning | Trend | Action |\n| --- | --- | --- | --- |\n| **`feature-gap`** | Bruno has no field to hold this Postman value, so it's dropped on import. | **Should shrink** as Bruno adds support. | **This is the list to focus on.** Each entry is a concrete, user-visible data loss. |\n| **`normalization`** | Intentional, semantically-lossless transformation. | **Permanent** — will never reach zero. | Leave alone. Documents *why* the round-trip isn't byte-identical. |\n\n**Where to focus: the `feature-gap` count.** It's the number of Postman auth capabilities Bruno can't\nrepresent. Driving it down is real product work; when a gap is filled, delete its whitelist entry and\nthe test stays green. The `normalization` entries are not a backlog — they're the documented contract\nof what round-tripping intentionally changes.\n\n### The two flavors of `normalization`\n\n- **Grant-scoped pruning** (oauth2): Bruno stores grant-type-specific fields, so a field unused by the\n  grant (`authUrl` on a `client_credentials` grant, `username` on anything but `password`) is dropped.\n  These entries carry a `grantType` list and are accepted **only** for grants that don't use the field —\n  so the *same* drop on a grant that *does* use it (e.g. `authUrl` on `authorization_code`) still fails.\n  That keeps the exception tight instead of blanket-allowing the field to vanish.\n- **Default materialization**: Postman omits a field and relies on a default; Bruno stores it\n  explicitly and re-emits it (apikey `in: header`, oauth1 `version`). Whitelisted only where **Bruno's\n  default equals Postman's default**, so materializing it changes nothing semantically.\n\n### Whitelist entry matching\n\nEach entry matches a diff on `(fixture, node, authType, key, kind)`:\n\n- `fixture` and `node` accept `'*'` — a field-level gap that recurs across many requests/fixtures is\n  **one wildcard entry**, not dozens of near-duplicates.\n- `grantType` (oauth2, optional) restricts an entry to specific grant types (grant-scoped pruning).\n- Matching is **value-agnostic** (`kind` + `key`, never the value) so a whitelisted field tolerates any\n  value — we're asserting \"this field is *expected* to differ\", not pinning a specific value.\n\n---\n\n## Comparison mechanics (`compare-auth.js`)\n\n### `stableStringify`\n\nAuth values are compared by a **canonical stringification** — object keys sorted, array order\npreserved — rather than by `JSON.stringify` or reference equality. Two reasons:\n\n1. **Key order is not semantic.** Postman and Bruno emit auth params in different orders; a plain\n   string compare would flag every reorder as a diff. Sorting keys makes the compare structural.\n2. **Nested values must compare by content.** The oauth2 `additionalParameters` arrays\n   (`authRequestParams` / `tokenRequestParams` / `refreshRequestParams`) are arrays of objects. We need\n   deep, order-insensitive-on-keys but order-*sensitive*-on-arrays comparison, which `stableStringify`\n   gives: object keys are sorted (order-insensitive), array element order is preserved (array order *is*\n   semantic for these params).\n\n### `normalizeAuth` — array-backed → map\n\nPostman's v2.1 auth is an array of `{key, value, type}`. We collapse it to a plain `{key: value}` map so\nthat (a) param ordering doesn't matter and (b) the `type` tag is ignored because it is not semantically meaningful. \nMissing auth normalizes to `null` so \"no auth\" compares equal on both sides.\n\n### `absent === \"\"` collapse\n\nAn absent key and an empty-string value are treated as **equal**. Postman treats a missing auth param as\nunset, and `brunoToPostman` drops empty params on export — so `state: \"\"` disappearing on round-trip is a\nlossless no-op, not a diff. Collapsing this structurally (rather than whitelisting each empty field)\nkeeps the whitelist focused on real differences and avoids churn when a fixture's empty fields change.\nConfirmed safe: Postman's importer defaults any missing auth param, so omitting empties never errors.\n\n**Exception — keys with a non-empty Postman default.** \"Absent == unset\" is false for a param Postman\nomits only because it sits at a non-empty default it still applies: oauth2 `headerPrefix` omitted means\n`Bearer`, so collapsing it would hide Bruno dropping the prefix. `POSTMAN_NON_EMPTY_DEFAULTS` in\n`compare-auth.js` materializes these on the *original* side (key absent or `null`), each with an\n`appliesWhen` predicate since the default is usually conditional — `headerPrefix` applies when the token\ngoes in the header, and an absent `addTokenTo` counts as header because Postman resolves an unspecified\nplacement to Request Headers. These predicates model **Postman's** defaults, never Bruno's importer:\nmatching the importer would make the oracle ratify it instead of checking it.\n\n### `grantType` on oauth2 diffs\n\nEach oauth2 diff is tagged with the node's grant type (from the original, falling back to the\nround-tripped side). This is what lets grant-scoped-pruning whitelist entries be conditional — the diff\ncarries enough context to say \"dropped `authUrl`, and this was a `client_credentials` grant, so it's\nfine.\"\n\n### Node-path walk\n\n`collectAuthNodes` walks the whole collection — collection-level auth, folder-level auth, and\nrequest-level auth — keying each by a stable path (`collection`, `collection/Folder`,\n`collection/Folder/Request`). Comparison is per-node by path, so a request that gains or loses auth, or\ninherits differently, is caught rather than averaged away.\n\n---\n\n## Running\n\n```bash\n# from packages/bruno-converters\nnpx jest tests/postman/round-trip/auth.spec.js\n```\n\nOn failure, each un-whitelisted diff prints a readable line — `DROPPED` (Bruno lost a field),\n`ADDED` (Bruno emitted a field Postman didn't), or `CHANGED` (value differs) — so you can tell at a\nglance whether it's a new feature gap to whitelist or a real regression to fix.\n","packages/bruno-docs/readme.md":"# bruno-docs\n\nThis is a wip.\n\nWe have a request to generate docs in a html file that can be hosted on a server so that the visitor can view the API and make requests without downloading/installing anything.","packages/bruno-electron/readme.md":"# bruno-electron\n\n```bash\n# electron dev\nnpm start\n\n# generate pfx file for signing windows build\nopenssl pkcs12 -export -inkey sectigo.key -in sectigo.pem -out sectigo.pfx\n```\n","packages/bruno-filestore/README.md":"# Bruno Filestore\n\nA generic file storage and parsing package for Bruno API client.\n\n## Purpose\n\nThis package abstracts the file format operations for Bruno, providing a clean interface for parsing and stringifying Bruno requests, collections, folders, and environments.\n\n## Features\n\n- Format-agnostic APIs for file operations\n- Currently supports Bruno's custom `.bru` format\n- Designed for future extensibility to support YAML and other formats\n\n## Usage\n\n```javascript\nconst {\n  parseRequest,\n  stringifyRequest,\n  parseCollection,\n  stringifyCollection,\n  parseEnvironment,\n  stringifyEnvironment,\n  parseDotEnv\n} = require('@usebruno/filestore');\n\n// Parse a .bru request file\nconst requestData = parseRequest(bruContent);\n\n// Stringify request data to .bru format\nconst bruContent = stringifyRequest(requestData);\n\n// Example with future format support (not yet implemented)\nconst requestData = parseRequest(yamlContent, { format: 'yaml' });\n```\n\n### Writing a converted collection to disk\n\n`writeCollectionToDisk` takes a collection as returned by `@usebruno/converters` and writes it as a collection directory that Bruno can open:\n\n```javascript\nconst { openApiToBruno } = require('@usebruno/converters');\nconst { writeCollectionToDisk } = require('@usebruno/filestore');\n\nconst collection = openApiToBruno(openApiSpecification);\nawait writeCollectionToDisk(collection, './my-collection');\n```\n\n`outputDir` is created if it does not exist. If it already exists it must be empty, otherwise the call rejects with an error whose `code` is `'ENOTEMPTY'` and nothing is written.\n\n## API\n\nThe package provides the following functions:\n\n- `parseRequest(content, options = { format: 'bru' })`: Parse request file content\n- `stringifyRequest(requestObj, options = { format: 'bru' })`: Convert request object to file content\n- `parseCollection(content, options = { format: 'bru' })`: Parse collection file content\n- `stringifyCollection(collectionObj, options = { format: 'bru' })`: Convert collection object to file content\n- `parseFolder(content, options = { format: 'bru' })`: Parse folder file content\n- `stringifyFolder(folderObj, options = { format: 'bru' })`: Convert folder object to file content\n- `parseEnvironment(content, options = { format: 'bru' })`: Parse environment file content\n- `stringifyEnvironment(envObj, options = { format: 'bru' })`: Convert environment object to file content\n- `parseDotEnv(content)`: Parse .env file content\n- `writeCollectionToDisk(collection, outputDir, options = { format: 'yml' })`: Write a whole collection to `outputDir` in `yml` or `bru` format. Resolves to `outputDir`. `outputDir` must be missing or empty; otherwise it rejects with `code: 'ENOTEMPTY'` ","packages/bruno-graphql-docs/readme.md":"# bruno-graphql-docs\n\nStandalone graphql docs explorer module forked from [graphiql](https://github.com/graphql/graphiql)\n\n### Publish to Npm Registry\n```bash\nnpm publish --access=public\n```","packages/bruno-js/readme.md":"# bruno-js\n\nProvides the script, test, vars and assert runtimes.\n\n### Publish to Npm Registry\n```bash\nnpm publish --access=public\n```","packages/bruno-lang/readme.md":"# bruno-lang\n\nThe language utils for working with `.bru` files\n\n### Publish to Npm Registry\n```bash\nnpm publish --access=public\n```","packages/bruno-query/readme.md":"# bruno-query\n\nBruno query with deep navigation, filter and map support\n\nEasy array navigation\n```js\nget(data, 'customer.orders.items.amount')\n```\nDeep navigation .. double dots\n```js\nget(data, '..items.amount')\n```\nArray indexing\n```js\nget(data, '..items[0].amount')\n```\nArray filtering [?] with corresponding filter function\n```js\nget(data, '..items[?].amount', i => i.amount > 20) \n```\nArray filtering [?] with simple object predicate, same as (i => i.id === 2 && i.amount === 20)\n```js\nget(data, '..items[?]', { id: 2, amount: 20 }) \n```\nArray mapping [?] with corresponding mapper function\n```js\nget(data, '..items..amount[?]', amt => amt + 10) \n```\n\n### Publish to Npm Registry\n```bash\nnpm publish --access=public\n```\n","packages/bruno-schema/readme.md":"# bruno-schema\n\nThe schema definition for collections\n\n### Publish to Npm Registry\n```bash\nnpm publish --access=public\n```\n\n### Collection schema\n```bash\nid                       Unique id (when persisted to a db)\nuid                      Unique id\nname                     collection name\nitems                    Items (folders and requests)\n  |-uid                  A unique id   \n  |-name                 Item name \n  |-type                 Item type  (folder, http-request, graphql-request)\n  |-request              Request object\n    |-url                Request url\n    |-method             Request method\n    |-headers            Request headers (array of key-val)\n    |-params             Request params (array of key-val)\n    |-body               Request body object  \n      |-mode             Request body mode\n      |-json             Request json body\n      |-text             Request text body\n      |-xml              Request xml body\n      |-multipartForm    Request multipartForm body\n      |-formUrlEncoded   Request formUrlEncoded body\n```\n","packages/bruno-sqlite/README.md":"# @usebruno/sqlite\n\nSQLite storage for the Bruno API client. You author migrations as `.ts` and statements as `.sql`; the package compiles them into a typed data layer for the Electron main process.\n\n## Entry point\n\n`@usebruno/sqlite` (or `/node`) — main process only. Owns the DB and runs statements, using the built-in `node:sqlite`.\n\n## Add a migration\n\n```bash\nnpm run migration:new --workspace=packages/bruno-sqlite   # prompts for a name\n```\n\nThis scaffolds a sequence-prefixed `migrations/<seq>_<name>.ts` file exporting `up` and `down`. Each returns the SQL to apply or revert the migration (a plain string also works):\n\n```ts\nexport const up = (): string => {\n  return 'CREATE TABLE users (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, email TEXT);';\n}\nexport const down = (): string => {\n  return 'DROP TABLE users;';\n}\n```\n\nMigrations apply automatically (in prefix order) when the DB opens, and their content is hashed into a `_migrations` table so an already-applied migration can't be silently edited.\n\n### Verify migrations\n\n`npm run migration:verify` replays every migration against a throwaway `VACUUM INTO` copy of a real database, so it exercises them over actual data without touching the source DB. Point it at the DB with an absolute `DB_PATH`:\n\n```bash\ncp .env.example .env      # then set DB_PATH to an absolute path\nnpm run migration:verify --workspace=packages/bruno-sqlite\n```\n\nIt fails if a migration errors, or if a migration's content no longer matches what was recorded when it was first applied to that DB.\n\n### Schema\n\n[`SCHEMA.md`](./SCHEMA.md) shows the schema the migrations produce: an ER diagram plus each table's columns, keys and indexes. The pre-commit hook regenerates it whenever a migration is staged; `npm run schema` regenerates it by hand.\n\n## Add statements (sqlc syntax)\n\nWrite them in `statements/*.sql` — multiple per file. Each starts with a `-- name:` annotation, and params are named (`@param`):\n\n```sql\n-- name: list_users :many\nSELECT id, name, email FROM users ORDER BY name;\n\n-- name: get_user :one\nSELECT id, name, email FROM users WHERE id = @id;\n\n-- name: create_user :exec\nINSERT INTO users (name, email) VALUES (@name, @email);\n```\n\n| annotation | runs as | returns |\n|---|---|---|\n| `:one` | single row | row or `undefined` |\n| `:many` | rows | array |\n| `:exec` | write | `{ changes, lastInsertRowid }` |\n\nRun `npm run generate` (or `build`) to compile statements into the typed registry.\n\n## Use it — main process\n\n```js\nconst { createDatabase } = require('@usebruno/sqlite');\n\nconst { db, statements } = createDatabase('/path/to/bruno.db');\n\nstatements.execute('create_user', { name: 'Ada', email: 'ada@example.com' });\nconst users = statements.execute('list_users');       // no params\nconst ada   = statements.execute('get_user', { id: 1 });\n// db.close() on shutdown\n```\n\nParams are always an **object** keyed by the named parameters (no positional/array binding).\n\nThe package does not expose statements to a renderer. The host application registers its own IPC handler for each statement it wants to expose.\n\n## Files\n\nRows are a poor home for a large payload, so the package ships a `files` table with a store in front\nof it. Each entry is kept in whichever shape fits its size:\n\n| size | stored as |\n|---|---|\n| up to 1MB | a `BLOB` on the row |\n| over 1MB | a file next to the database, with the row pointing at it |\n\n`createDatabase` returns the store alongside the statements:\n\n```js\nconst { db, statements, files } = createDatabase('/path/to/bruno.db');\n\nconst entry = await files.write(buffer, { contentType: 'image/png' });  // { id, size, inline, contentType }\nconst bytes = await files.read(entry.id);                                // Uint8Array, or null\nfiles.stat(entry.id);\nawait files.remove(entry.id);\n```\n\n`read` returns the whole payload, from the row or from the spilled file.\n\nThe files directory defaults to `<database path minus extension>-files`; pass `filesDir` to place it\nelsewhere, and `inlineMaxBytes` to move the threshold.\n\n### Lifetime\n\nA file lives for as long as some row points at it. Declare that with a foreign key:\n\n```sql\nALTER TABLE runner_responses ADD COLUMN body_file_id INTEGER REFERENCES files(id);\n```\n\n`files.collect()` reads the foreign keys back off the schema, deletes every row no table references,\nremoves the files they own, and sweeps files on disk that no row names. Nothing has to be registered\nwith the store — adding the column is enough. Call it at startup.\n\nAs a safety rule, when *no* table references `files` the store leaves rows alone: with no referrers\nevery row would look unreferenced. The disk sweep still runs.\n\n### Reading files from the renderer\n\nThe package exposes no renderer API for files, just as for statements. Writes are main-process only; to let the renderer read a file, the host registers its own IPC handler for that read.\n\n## Development\n\n```bash\nnpm run build --workspace=packages/bruno-sqlite    # generate + bundle + types\nnpm run watch --workspace=packages/bruno-sqlite    # rebuild on change\nnpm run test  --workspace=packages/bruno-sqlite    # jest\n```\n\n`.sql` files are the only source of truth committed; `src/generated/` and `dist/` are git-ignored and produced by the build.\n","packages/bruno-tests/collection/readme.md":"# bruno-tests collection\n\nAPI Collection to run sanity tests on Bruno CLI.\n","packages/bruno-tests/collection_level_oauth2/readme.md":"# bruno-tests collection\n\nAPI Collection to run sanity tests on Bruno CLI.\n","packages/bruno-tests/collection_oauth2/readme.md":"# bruno-tests collection\n\nAPI Collection to run sanity tests on Bruno CLI.\n","packages/bruno-tests/readme.md":"# bruno-tests\n\nThis package is used to test the Bruno CLI.\nWe have a collection that sits in the `collection` directory.\n\n### Test Server\n\nThis will start the server on port 80 which exposes endpoints that the collection will hit.\n\n```bash\n# install node dependencies\nnpm install\n\n# start server\nnpm start\n```\n\n### Run Bru CLI on Collection\n\n```bash\ncd collection\n\n# run collection against local server\nnode ../../bruno-cli/bin/bru.js run --env Local --output junit.xml --format junit\n\n# run collection against prod server hosted at https://testbench.usebruno.com\nnode ../../bruno-cli/bin/bru.js run --env Prod --output junit.xml --format junit\n```\n\n### License\n\n[MIT](LICENSE)\n","packages/bruno-tests/reports/README.md":"# Bruno CLI — Docker run reports\n\nThis folder is the bind-mount target for the Docker Compose example at [`../docker-compose.yml`](../docker-compose.yml).\n\nWhen you run:\n\n```bash\ncd packages/bruno-tests\ndocker compose run bruno-cli\n```\n\nthe container writes three report files into this directory:\n\n| File | Format | Purpose |\n|------|--------|---------|\n| `results.json` | JSON | Machine-readable run summary (request/response, timings, assertion results) |\n| `results.xml` | JUnit XML | For CI test reporters (GitHub Actions, GitLab CI, Jenkins, etc.) |\n| `results.html` | HTML | Human-readable report you can open in a browser |\n\nThe files themselves are gitignored — only this `README.md` and the `.gitignore` are tracked, which keeps the folder present in the repo so the `./reports:/reports` bind mount has somewhere to land without needing manual `mkdir` first.\n\nTo skip a format, drop the corresponding `--reporter-*` flag from the `command:` block in `docker-compose.yml`.\n"},"items":[{"name":"CLAUDE.md","path":".claude/CLAUDE.md","rawUrl":"https://raw.githubusercontent.com/usebruno/bruno/HEAD/.claude/CLAUDE.md","title":".claude - Claude Agent Guidelines & System Prompt","category":"claude-rule","format":"markdown","content":"# Project Guide\n\nBruno fork — open-source API client (Electron + React + Redux).\n\nBruno is an API **client**, not a server: it sends requests and handles responses (auth, params, headers, bodies, environments, cookies). Judge every behavior and edge case by \"what should a *client* do here?\" — surface malformed or hostile responses clearly and stay robust; don't enforce server-side correctness.\n\n## Quick Commands\n\n```bash\nnpm run dev              # Run electron + react concurrently\nnpm run dev:watch        # Same, with hot-reload of the electron main process\nnpm run storybook        # Component dev (bruno-app)\nnpm run lint:fix         # ESLint fix\nnpm test --workspaces    # Unit tests (Jest) across all packages\n```\n\nFull script list: root `package.json`. Setup: `nvm use && npm i --legacy-peer-deps && npm run setup` (Node **v22.12.0**, see `.nvmrc`).\n\n### Running a single test\n\n```bash\n# Single unit test (Jest) — scope to the workspace, pass a path/pattern after --\nnpm test --workspace=packages/bruno-app -- path/to/file.spec.js\nnpm test --workspace=packages/bruno-requests -- -t \"test name pattern\"\n\n# Single e2e test (Playwright) — a project flag is REQUIRED (config has no default)\nnpx playwright test tests/collection/create-collection.spec.ts --project=default\nnpx playwright test --project=default -g \"test name pattern\"\n```\n\nPrefer the smallest scope — one workspace, one spec — over `--workspaces` or the full e2e run.\n\n### Building shared packages\n\n`npm run dev` does **not** rebuild the shared packages (bruno-common, bruno-requests,\nbruno-filestore, bruno-sqlite, bruno-converters, bruno-query, bruno-schema-types, bruno-graphql-docs).\nAfter editing one, rebuild it or run its watcher, or the app won't pick up changes:\n\n```bash\nnpm run build:bruno-common      # also :bruno-requests, :bruno-filestore, :bruno-sqlite, etc.\nnpm run watch:common            # also watch:requests, watch:converters\n```\n\n## Key Architecture\n\n- **Main process entry**: `packages/bruno-electron/src/index.js`\n- **Renderer + Redux store**: `packages/bruno-app/src/providers/ReduxStore/`\n- Packages live under `packages/`, with top-level `tests/` and `playwright/`.\n\n## Testing\n\n- **Unit**: Jest, config per-package (`packages/*/jest.config.js`); bruno-app uses jsdom + `jest.setup.js`.\n- **E2E**: Playwright — fixtures, helpers, isolation rules, and pitfalls live in\n  `.claude/rules/testing.md`; use the `write-e2e-test` skill when adding specs.\n\n## Coding Standards\n\nFull list: `CODING_STANDARDS.md`. Mechanical style (indent, quotes, semicolons, no trailing\ncommas, arrow-param parens) is ESLint-enforced — run `npm run lint:fix`, don't hand-police it.\nNon-obvious project rules worth holding:\n\n- React: avoid `useEffect` (prefer derived state / custom hooks); import hooks by name, never `React.useX`; a component is controlled XOR uncontrolled; Tailwind for layout, styled-components `theme` for colors.\n- Every component owns a directory — `<ComponentName>/index.js(x)`, never `<ComponentName>.js(x)`.\n- Extract for readability or clear reuse; avoid only indirection that adds no payoff. Add `data-testid` attributes for Playwright selectors.\n\n## Detailed rules & references\n\nPath-scoped rules in `.claude/rules/` auto-attach when you touch matching files:\n`architecture.md` (`@usebruno/*` dependency boundaries, manifests), `bruno-app-layout.md` (bruno-app\nmodule layout), `electron-ipc.md` (IPC handlers + startup), `redux-store.md` (slices/middleware),\n`sqlite.md` (`@usebruno/sqlite` — migrations, statements, per-statement IPC), `testing.md` (e2e\npatterns & gotchas), `cross-platform.md` (Windows file/process/path pitfalls), `dsl-changes.md`\n(on-disk `.bru`/`.yml` format & backward compat), `feature-parity.md` (co-related features —\napp↔CLI twins, converter/shim/level/protocol pairs), `conventions.md` (readability, reuse,\nreplacement & pre-submit hygiene).\n\nRead on demand (not auto-loaded): `.claude/reference/architecture.md` — the monorepo map, request\npipeline, sandbox, core types, and dependency versions. Consult it before cross-package or\narchitectural work.\n","isInternal":false,"tokens":1035,"sizeBytes":4109},{"name":"architecture.md","path":".claude/rules/architecture.md","rawUrl":"https://raw.githubusercontent.com/usebruno/bruno/HEAD/.claude/rules/architecture.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"---\npaths:\n  - \"packages/**/*\"\n---\n\n# Dependency Boundaries\n\nInternal `@usebruno/*` dependencies (from each `package.json`) form a strict DAG. **New code must\nrespect it** — a cyclic or upward dependency is an architectural bug, not a convenience.\n\nThe full monorepo map (build tools, request pipeline, sandbox, file formats, core data-model\ntypes, dependency versions) is the on-demand reference `.claude/reference/architecture.md` — read\nit before non-trivial cross-package or architectural work.\n\n## Dependency direction & ownership boundaries\n\n- **Leaf libs — zero internal `@usebruno/*` deps:** bruno-common, bruno-lang, bruno-query,\n  bruno-requests, bruno-graphql-docs, bruno-schema, bruno-schema-types, bruno-toml, bruno-sqlite\n  (a leaf, but *not* platform-neutral — see guardrail 6).\n- **Mid consumers:** bruno-js → (common, query); bruno-converters → (common, schema; schema-types\n  as devDep); bruno-filestore → (common, lang; schema-types as devDep).\n- **Top consumers (things flow *into* them, never out):** bruno-cli → (common, converters,\n  filestore, js, lang, requests); bruno-electron → (common, converters, filestore, js, lang,\n  requests, schema, sqlite); bruno-app → (common, converters, graphql-docs, schema).\n\nGuardrails this enforces:\n\n1. **bruno-common is the browser-safe base leaf.** It runs in the web renderer (`bruno-app`), not\n   just Node, so it must stay platform-neutral: no Node built-ins (`fs`, `path`, `os`, `crypto`,\n   `child_process`, `node:*`) and no dependency that itself pulls in Node. It currently ships with\n   zero runtime dependencies — keep it that way. It also depends on no other `@usebruno/*` package;\n   a util that needs Node or another bruno package belongs in a different package.\n2. **No shared/library package may import bruno-app or bruno-electron.** Renderer- or\n   Electron-specific code must not be pushed down into a lib to make it importable upward.\n3. **bruno-js must stay Electron-free.** It runs in both the Electron main process *and* the CLI\n   (bruno-cli → js). Adding an `electron`/IPC import to bruno-js breaks the CLI. Sandbox logic\n   belongs in bruno-js; host wiring belongs in bruno-electron.\n4. **bruno-schema-types is types-only** (a *devDependency* of converters/filestore, built with\n   plain `tsc`). Import types from it; never add runtime code or a runtime import of it.\n5. **bruno-schema (Yup) and bruno-schema-types (TS types) are distinct and both live.** app +\n   converters use `@usebruno/schema` for runtime validation; filestore + converters use\n   `@usebruno/schema-types` for compile-time types. A data-model change usually touches both.\n6. **bruno-sqlite is main-process only.** `@usebruno/sqlite` (= `/node`) imports `node:sqlite`,\n   `node:fs` and `node:crypto`; importing it from the renderer breaks the bundle. The renderer\n   reaches statements only through the per-statement IPC handlers in bruno-electron. See\n   `.claude/rules/sqlite.md`.\n\n## Declared dependencies must match real imports\n\nA package's `package.json` is the contract; workspace hoisting hides breaches of it. Declare every\nimport in the manifest of the package that imports it — one resolving only because a sibling or the\nroot happens to pull it in is a latent break. Test- and build-only packages belong in\n`devDependencies`; anything in `dependencies` ships to users with its transitive tree. Drop a\ndeclaration when the change removes its last consumer.\n","frontmatter":{"paths":["packages/**/*"]},"isInternal":false,"tokens":863,"sizeBytes":3451},{"name":"bruno-app-layout.md","path":".claude/rules/bruno-app-layout.md","rawUrl":"https://raw.githubusercontent.com/usebruno/bruno/HEAD/.claude/rules/bruno-app-layout.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"---\npaths:\n  - \"packages/bruno-app/**/*\"\n---\n\n# Module & Directory Layout (bruno-app)\n\nLayout is predictable enough here to be load-bearing, so a misplaced file is an architectural defect,\nnot a style nit — a breach blocks, the same as a dependency-direction violation in\n`.claude/rules/architecture.md`.\n\n- **A component owns a directory: `<ComponentName>/index.js(x)`, never `<ComponentName>.js`** — at\n  every nesting depth, including a sub-component of a component. Same shape for hooks\n  (`src/hooks/useX/index.js`) and `src/ui/` primitives.\n- **Everything a component owns lives in its directory** beside its `index.js` — `StyledWrapper.js`,\n  spec, component-specific utils. Reaching up into a parent's `StyledWrapper.js`, or leaving a\n  component's styles outside its directory, hands ownership of them to no one.\n- **`src/ui/` is a leaf**: app-agnostic presentational primitives only, and it must not import from\n  `src/components/` — the edge runs `components → ui`, never back. A `ui/` module needing one either\n  isn't a primitive, or its dependency belongs in `ui/` too.\n- **`utils/` is for stateless helpers you call.** A module *registered* into something — an editor\n  extension, a plugin, a node view, a keymap — is behavior or configuration, and filing it under\n  `utils/` hides that it's part of a schema or document contract. Give those their own directory.\n\nExisting breaches — bare `PascalCase.js(x)` component files, and `ui/MenuDropdown`'s import of\n`components/Dropdown` — are drift, not a second pattern. Don't extend them, and don't raise an\nuntouched one as a finding.\n","frontmatter":{"paths":["packages/bruno-app/**/*"]},"isInternal":false,"tokens":397,"sizeBytes":1615},{"name":"conventions.md","path":".claude/rules/conventions.md","rawUrl":"https://raw.githubusercontent.com/usebruno/bruno/HEAD/.claude/rules/conventions.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"---\npaths:\n  - \"packages/**/*\"\n  - \"tests/**/*\"\n  - \"scripts/**/*\"\n---\n\n# Coding Conventions, Readability & Hygiene\n\n`CODING_STANDARDS.md` is the source of truth for Bruno's coding standards — read it. This file is\nthe judgment layer over those standards: the readability and code-hygiene calls a linter can't make.\nCode and comments must read as a natural, permanent part of the project — never as artifacts of the\ntask or session that produced them.\n\n## Style & formatting\n\nThe mechanical style — 2-space indent, single quotes (double for JSX/TSX attributes), semicolons,\nno trailing commas, parenthesized arrow params, spacing around arrows, no space before call parens\n— is enforced by ESLint and auto-fixed by `npm run lint:fix`. These deviations are real but\nlow-value to catch by hand: note them briefly rather than dwelling. Naming and casing that ESLint\ncan't mechanically repair still warrant attention.\n\n## Readability\n\n- **Descriptive names.** Functions and variables carry concise, descriptive names; an unclear or\n  misleading name is worth raising even when the code is otherwise correct.\n- **Reuse before you write.** Before adding a component, hook, or helper, search for the one that\n  already exists — by concept, not by the name you'd have picked — and read the nearest sibling\n  solving the same shape of problem, since its call site shows the intended composition. Reuse is\n  usually a *net deletion* — the bespoke markup and its CSS go. Where the existing primitive is\n  *almost* right, widen it rather than standing up a near-duplicate beside it; two near-identical\n  implementations diverge silently.\n- **Extraction & abstraction.** Extract a helper or shared abstraction whenever it genuinely\n  improves readability or serves a clear, anticipated reuse — this is encouraged and not gated on a\n  minimum number of call sites; suggest it where it would help. Avoid only *unnecessary* abstraction:\n  a layer that adds indirection without improving clarity or earning reuse — a generalized utility\n  built for a single site with no foreseeable second user, or options/config added \"for later.\"\n  Breaking a long, complex function into well-named local helpers for readability is always fine.\n- **Single-line indirection.** A one-line function that only forwards to another — adding a stack\n  frame without adding meaning — should be inlined.\n- **Optional chaining.** `?.` belongs only where the null case is handled right there (a fallback,\n  early return, or guard). Used elsewhere it hides whether a value can genuinely be null and works\n  against TypeScript's guarantees; fix the type or narrow first.\n- **Comments explain the why.** Genuinely complex flow deserves a comment covering the rationale an\n  obvious reading can't; self-explanatory code does not. Full bar in **Comments** below.\n- **Functional, but readable.** Prefer obvious, linear pipelines over deep functional machinery\n  (ADTs, monads) — the code should stay easy for any contributor to follow and extend.\n\n## Comments\n\n- **No situational or prompt-driven comments.** A comment must not reference the change, the task, or\n  the moment it was written. Drop `// added to fix ...`, `// as requested`, `// new logic for X`,\n  `// updated to handle ...`, `// per review`. If the reason genuinely matters, state it as a\n  timeless fact about the code (or link the issue/PR) — not as \"what I just did\".\n- **No obvious comments.** Don't restate what the code already says. `// loop over items` above a\n  loop, `// set the name` above `obj.name = name`, `// return the result` — these add nothing. If the\n  code is self-explanatory, leave it uncommented.\n- **Comment the why, not the what.** Reserve comments for what the code can't show: non-obvious\n  rationale, invariants, edge cases, a workaround and the constraint forcing it, units, or a pointer\n  to a spec/issue. These stay useful long after the change lands.\n- **No scaffolding or narration.** No `// ... existing code ...`, no placeholder or TODO-for-me\n  notes, no changelog or step-by-step narration in comments, no commented-out code left behind.\n\n## Beyond comments\n\n- **Anything added needs a live consumer in the same change.** No options, parameters, or\n  configuration \"for later\"; no payload field no reader destructures, branch for a state the producer\n  can't emit, or ignored parameter. Each is dead on arrival and reads as a contract honored somewhere\n  else — if the consumer is a follow-up, leave it out. Extracting a helper for readability or clear,\n  foreseeable reuse is fine (see **Readability → Extraction & abstraction** above).\n- Match the surrounding code's style and naming so a change is indistinguishable from the existing\n  codebase, not visibly bolted on.\n- Keep diffs minimal — no unrelated reformatting or whitespace churn. A drive-by cleanup worth doing\n  belongs in its own commit, and never in a *different package* than the change is about; interleaved,\n  reviewers can't separate scope creep from load-bearing edits.\n\n## Replacing code leaves nothing behind\n\nHalf a migration reads as a complete one — the leftovers survive review and mislead whoever edits the\nfile next. Closing the loop is part of the change.\n\n- **Follow what you replaced to every reference and remove it there** — the CSS whose selectors the\n  new markup no longer emits, the import whose last use just went, the prop nothing passes. Styles are\n  the easiest to leave behind and the most misleading when left: CSS for a class that renders nothing\n  fails silently and looks intentional.\n- **Don't copy a block to its new home and leave the original.** Where both copies must genuinely\n  exist — separate call sites, or one rule enforced on both sides of a process boundary — name the\n  invariant in a comment at both, because nothing else keeps them in sync.\n- **Reconcile the whole flow, not the entry point.** Reshaping something that crosses a boundary — a\n  payload, a serialized field, a lookup key — means walking to every consumer on the far side.\n  Deleting a control (a sanitizer, a validation or escaping step) is a decision to state, not a side\n  effect of a rewrite.\n\n## Before you call it done\n\n- **Run the affected workspace's tests**, not just the specs you wrote.\n- **Changed a return value or payload shape?** Other specs assert it too — exact-equality assertions\n  elsewhere break on an added key.\n- **Walk the acceptance criteria against the diff one at a time.** Compound criteria are what slip:\n  the half of the sentence you didn't have open never got exercised.\n","frontmatter":{"paths":["packages/**/*","tests/**/*","scripts/**/*"]},"isInternal":false,"tokens":1475,"sizeBytes":6583},{"name":"cross-platform.md","path":".claude/rules/cross-platform.md","rawUrl":"https://raw.githubusercontent.com/usebruno/bruno/HEAD/.claude/rules/cross-platform.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"---\npaths:\n  - \"scripts/**/*\"\n  - \"packages/bruno-electron/**/*\"\n---\n\n# Cross-Platform Guidelines\n\nThis is an Electron app targeting macOS, Windows, and Linux. All code touching the filesystem, child processes, signals, or paths must work on all three platforms.\n\n## Filesystem\n\n- `fs.rmSync` with `force: true` only suppresses `ENOENT`, NOT `EPERM`/`EBUSY`. Windows locks files aggressively (antivirus, indexing, open handles). Always use `maxRetries` and `retryDelay` when deleting directories.\n- Use `path.join()` / `path.resolve()` for all paths — never hardcode `/` separators.\n- Windows paths are case-insensitive. Use `normalizePath()` (from `utils/common/path`) when comparing collection/workspace paths.\n- `app.getPath()` returns platform-specific directories (`userData`, `documents`, etc.). Never assume POSIX-style locations.\n- File watchers (chokidar) may emit different event orderings across platforms. Don't depend on specific add/change/unlink sequences.\n\n## Child Processes\n\n- `spawn('npm', [...])` requires `shell: true` on Windows because `npm` is `npm.cmd`.\n- `child.kill()` with `shell: true` only kills the shell wrapper (`cmd.exe`) on Windows, leaving orphaned children. Use `taskkill /pid <pid> /T /F` to kill the entire process tree on Windows.\n- `execSync` / `spawn` commands must avoid Unix-only syntax (`&&` chains work in cmd.exe, but pipes and redirects differ).\n\n## Signals & Shutdown\n\n- `SIGINT` / `SIGTERM` are unreliable on Windows with `shell: true`. Also handle `SIGHUP` as fallback.\n- App shutdown must close all file watchers. Each watcher class implements `closeAllWatchers()`, orchestrated by `closeAllWatchers()` in `index.js`.\n\n## Line Endings\n\n- Files authored on Windows use CRLF. When parsing multiline `.bru`/text blocks line by line, split with a CRLF-aware regex (`/\\r\\n|\\r|\\n/`), never on `\\n` alone — a trailing `\\r` otherwise leaks into parsed values and causes spurious dirty-state and diffs. the `.split(/\\r\\n|\\r|\\n/)` in `bruno-lang`'s `v2/src/envToJson.js` is the reference pattern; keep new parsers consistent with it.\n\n## stdout vs stderr\n\n- Dev tools (rsbuild, webpack, electron-builder) may route startup output to stderr on Windows. When detecting patterns in process output, check both streams.\n\n## Platform-Specific Dependencies\n\n- `forceInstallPlatformDeps()` in `scripts/setup.js` installs platform-specific native modules (e.g., `@lydell/node-pty-{platform}-{arch}`).\n- Electron builder config handles platform-specific packaging (`mac`/`win`/`linux` targets).\n\n## Path Separators in Collection/Workspace Stores\n\n- electron-store persists paths as-is. A workspace opened on macOS stores `/Users/...`, on Windows `C:\\Users\\...`.\n- `ELECTRON_USER_DATA_PATH` only applies when `isDev` is true (`index.js:22`).\n","frontmatter":{"paths":["scripts/**/*","packages/bruno-electron/**/*"]},"isInternal":false,"tokens":678,"sizeBytes":2777},{"name":"dsl-changes.md","path":".claude/rules/dsl-changes.md","rawUrl":"https://raw.githubusercontent.com/usebruno/bruno/HEAD/.claude/rules/dsl-changes.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"---\npaths:\n  - \"packages/bruno-app/**\"\n  - \"packages/bruno-electron/**\"\n  - \"packages/bruno-cli/**\"\n  - \"packages/bruno-lang/**\"\n  - \"packages/bruno-filestore/**\"\n  - \"packages/bruno-schema/**\"\n  - \"packages/bruno-schema-types/**\"\n  - \"packages/bruno-converters/**\"\n---\n\n# On-Disk DSL & Serialization Changes\n\nBruno persists everything users create — collections, requests, environments, folders, and\nconfig — as plain-text files on the user's disk and in their Git repos. Two on-disk formats\nexist:\n\n- **`.bru`** — the Bru DSL. Read/write goes through the **v2 (ohm-js)** grammar in\n  `packages/bruno-lang/v2/src`, via `packages/bruno-filestore/src/formats/bru`\n  (`bruToJsonV2` / `jsonToBruV2`). Make all `.bru` changes in v2.\n- **`.yml`** — OpenCollection YAML, handled by `packages/bruno-filestore/src/formats/yml`. This is\n  the current `DEFAULT_COLLECTION_FORMAT` (canonical: `packages/bruno-filestore/src/constants.ts`,\n  duplicated for the renderer in `packages/bruno-app/src/utils/common/constants.js`), so new\n  collections are `.yml` unless the user chooses otherwise.\n- Plus `bruno.json` (per-collection config, passed through `stringifyCollection`) and `.env` files\n  (`parseDotEnv` / `dotenvToJson`).\n\n(`packages/bruno-toml` exists but is **orphaned** — imported nowhere outside its own test and on no\nserialization path; a DSL change never needs it. `preferences.json` is app-level electron-store\nstate under `userData`, not part of a collection's on-disk contract, so it is out of scope here.)\n\nThese files outlive the version that wrote them: an older Bruno reads files written by a\nnewer one, teammates on different versions share the same repo, and users hand-commit these\nfiles. **A change to the on-disk shape is a change to a public contract.** Get it wrong and\nyou break parsing, corrupt collections, or silently drop user data — on collections you\ncan't reach to fix.\n\nThe in-memory objects that become these files are often assembled far from the serialization\nlayer — in `bruno-electron` (IPC handlers) or `bruno-app` (Redux) — and handed to\n`bruno-filestore`, which can't tell that a field's shape changed upstream. So a DSL-affecting\nchange can originate in *any* package on the data path, not just the format packages. Treat\nthis rule as in scope for changes anywhere the collection/request/environment/config object\nis built, mutated, or serialized — the serialization packages plus `bruno-app`,\n`bruno-electron`, and `bruno-cli`, where these objects are assembled or read/written. Adding a\npersisted field in app/electron without wiring it through `bruno-filestore` (both formats) and\n`bruno-schema` silently drops or rejects data on save — the exact bug this rule guards against.\n\n## Rules\n\n**Avoid unnecessary DSL changes.** First ask whether the need can be met in memory, in the\nUI, or as a derived value without touching the serialized shape at all — most enhancements\ncan. Only change the on-disk format when the data genuinely must persist.\n\n**Additive and optional only.** A new field must be optional with a safe default, so files\nwithout it still parse and behave. Never rename, remove, repurpose, or retype an existing\nfield, and never change its semantics — old files and old app versions depend on the\ncurrent meaning.\n\n**Round-trip must be lossless.** `parse(stringify(x))` must equal `x`, and `stringify` must\nnever drop fields it doesn't recognize. A file written by a newer version, opened and\nre-saved by an older one, must not lose the newer fields.\n\n**Prove escaping on the written bytes, not in memory.** When a value is embedded in a\ndelimited block (`.bru` multiline `'''…'''`, annotation args), an in-memory `escape`/`unescape`\nround-trip can pass while the on-disk file still corrupts — the parser terminates at the first\nre-introduced delimiter. Test `parse(read(serialize(x)))` on the actual bytes, and fuzz across\nquote/backslash runs. Escape at single-character granularity: escaping a multi-character\ndelimiter by splitting on the whole delimiter (`split(\"'''\")`) leaves fragments that recombine\non runs of length ≥ the delimiter; escape the backslash first, then every single delimiter char.\nRef: `escapeMultilineDescription` in `bruno-lang/v2/src/utils.js`.\n\n**New syntax breaks older parsers — not just older fields.** The additive-and-optional rule\nprotects field-level compat, but adding new `.bru` *syntax* (a new escape form, block delimiter,\nor annotation grammar) makes files written by the new version fail to *parse* in older versions,\nwhich have no handling for it. This is a forward-compat break beyond field loss; it needs an\nexplicit decision and a compatibility path before shipping.\n\n**Descriptions are string-only internally.** Every inline `description` field (headers, params,\nassertions, variables, body entries, ...) is stored as a plain `string`. On the **yml** side,\nparsers accept a bare string *or* a legacy `{ content }` object on read and normalize to the\nstring (`formats/yml/common/{headers,variables,assertions,actions}.ts`, `parseEnvironment.ts`,\n`body.ts`); writers emit a bare string. This read-accepts-object / write-emits-string asymmetry is\nan established convention — not a lossy round-trip — proven by `formats/yml/common/variables.spec.ts`.\nThe `.bru` side has no `{ content }` handling; it stores descriptions as annotation/textblock\nstrings straight from the grammar. New inline description-like fields should follow this\nstring-only shape.\n\n*Exception — `docs` is not string-only.* Collection/folder documentation (`docs`) is written by\nthe **yml** layer as an **object** `{ content, type: 'text/markdown' }`\n(`formats/yml/stringifyCollection.ts`, `stringifyFolder.ts`) and normalized back to a string on\nread; the `.bru` side writes it as a plain textblock. So \"follow the string-only shape\" applies to\ninline `description`, *not* to `docs`-style documentation fields — check which family a new field\nbelongs to before copying either pattern.\n\n**Keep both formats in lockstep.** Any field added or changed must be handled in *both*\n`bru` and `yml` — parse and stringify on each side — or the same collection behaves\ndifferently depending on its format. There is **no dedicated `bru`↔`yml` converter**: a request\nmoves between formats by being parsed in its source format to the shared in-memory object and\nre-stringified in the target format (see `SaveTransientRequest` passing `sourceFormat` +\n`targetFormat` to `renderer:save-transient-request`). So a field handled in only one format is\n**silently dropped** the moment a request is copied/saved from a `.bru` collection into a `.yml`\none (or vice-versa). Pay attention to the *unset* case: a field that defaults or persists one way\nin `.bru` and another in `.yml` (e.g. one path returns `''`, the other `'1'`) is a divergence bug.\nThe default app-data workspace and custom-filesystem workspaces are a second such twin — a value\nthat persists in one but is silently dropped in the other is the same class of bug. Update every\nlayer the field touches: `bruno-schema-types` (types), `bruno-schema` (Yup validation),\n`bruno-filestore` (both formats), `bruno-converters` (import/export), and the grammar in\n`bruno-lang` **v2** if `.bru` syntax itself changes.\n\n**`bruno-schema` (Yup) is not optional — it will reject your field.** The collection/request/\nenvironment Yup schemas are declared `.noUnknown(true).strict()`, and they are validated on the\n**save path** (e.g. `itemSchema.validate` / `environmentSchema.validateSync` in\n`bruno-app`'s `slices/collections/actions.js` and `bruno-electron`'s `store/global-environments.js`).\nA new serialized field that round-trips through filestore but is **not** added to\n`packages/bruno-schema` will throw a validation error on save. `bruno-schema` (runtime Yup) and\n`bruno-schema-types` (TS types) are distinct and both must be updated.\n\n**When a shape must change, migrate on read — never make users edit files.** Add a\nread-time compatibility shim that upgrades the old shape as it is parsed, following the\nexisting patterns: `ensureAuthV3Rc1BackwardsCompatibility` in `formats/yml/parseItem.ts`,\nand the pre-v3 status/statusText swap in `formats/bru/index.ts`. Gate any eventual removal\nbehind a major version bump and leave a dated `TODO(remove after vN)`.\n\n**Design for scale and consistency.** New keys follow the naming and nesting of the existing\n`meta`/section blocks (`meta.name`, `meta.seq`, `meta.type`, `meta.tags`, ...). Prefer\nstructures that extend cleanly — a keyed list over a positional one, an object over a\nwidening union — and match how neighbouring fields are already modelled. Note the on-disk\n`meta {}` block does **not** map 1:1 to the type: in `bruno-schema-types` those keys are flattened\nonto `Item` (`seq`, `name`, `type`, `tags`, `description` directly on the item), and the v2 ohm\ngrammar treats `meta` as an open dictionary — the four names above are the *modeled* ones, not a\nclosed set.\n\n**Name properties like they're permanent — because they are.** A DSL key is written once and\neffectively forever: renaming it breaks every existing file and forces a compat shim. These\nnames are also user-facing — people read and hand-edit `.bru`/`.yml` — and are read by AI\nagents reasoning over collections. Choose clear, descriptive, fully spelled-out names, with\nno cryptic abbreviations or ambiguity, consistent with existing keys, and get it right the\nfirst time. A property name deserves stricter scrutiny than an ordinary variable.\n\n**Test the contract.** Add round-trip tests (parse → stringify → parse) for the new field in\n*both* formats, plus a golden fixture of the *old* format that must still parse unchanged. The\npaired `parseItem.spec.ts` / `stringifyItem.spec.ts` (and `parse*`/`stringify*` for collection,\nfolder, environment) next to each serializer in `bruno-filestore/src/formats/yml` are the pattern;\n`common/variables.spec.ts` and `common/datatype.spec.ts` show field-level cases.\n\n## A new persisted field — the files it usually touches\n\nAdding a single field tends to span several packages (a `.bru`-centric change lands mostly in\n`bruno-lang/v2`):\n\n- `bruno-lang/v2/src` — the `.bru` entry points: `bruToJson.js`, `collectionBruToJson.js`,\n  `envToJson.js`, `jsonToBru.js`, `jsonToCollectionBru.js`, `jsonToEnv.js`, plus `utils.js` for\n  shared helpers.\n- `bruno-filestore/src/formats/yml` — parse **and** stringify for the `.yml` side.\n- `bruno-schema/src/collections/index.js` — the Yup schema (skip this and it fails on save).\n- `bruno-schema-types/src/collection/item.ts` — the TypeScript type.\n- `bruno-converters` — a **separate field list from `bruno-filestore`**, not the same code path:\n  `src/opencollection/items/{http,graphql,grpc,websocket}.ts` maps the field for the OpenCollection schema, might have typescript issues to be consulted with https://github.com/opencollection-dev/opencollection when needed\n  export *and* import, and `src/postman/bruno-to-postman.js` for Postman export. A field wired\n  through filestore but not here round-trips on disk and is silently lost on export.\n- `bruno-app/src/utils/collections/index.js` — `transformRequestToSaveToFilesystem` and siblings\n  whitelist fields before save; a field missing here never reaches the serializer.\n- `bruno-electron/src/ipc/yml-migration.js` — BRU → YML migration (bru parse → yml stringify, plus\n  `bruno.json` → `opencollection.yml` config transforms); verify the field survives migration.\n- the collection utils in `bruno-electron` / `bruno-app` that build the object.\n- Round-trip specs next to the serializers (`formats/yml/parseItem.spec.ts` + `stringifyItem.spec.ts`).\n\nFind a comparable field already in the code and follow how it's wired before adding yours.\n\n## Before changing the DSL — checklist\n\n- [ ] Change is genuinely necessary (can't be solved in memory / UI)\n- [ ] New field is optional with a safe default; nothing renamed, removed, or retyped\n- [ ] Handled in both `bru` and `yml`, parse **and** stringify\n- [ ] Types (`bruno-schema-types`), Yup schema (`bruno-schema`), and converters updated —\n      \"converters\" means `bruno-converters/src/opencollection/items/*` in **both** directions,\n      which is a different field list from `bruno-filestore`\n- [ ] Field survives the app save transform and BRU → YML migration (see `feature-parity.md` §2)\n- [ ] Old files still parse — read-time compat shim added if the shape changed\n- [ ] No new `.bru` syntax that older parsers can't read (or a forward-compat path decided)\n- [ ] Any new escaping proven via on-disk reparse + fuzz, escaping single chars not the delimiter\n- [ ] `bru`/`yml` (and default vs custom workspace) behave identically, including the unset case\n- [ ] Round-trip + old-format fixture tests added\n- [ ] Naming/structure consistent with existing blocks and scalable\n","frontmatter":{"paths":["packages/bruno-app/**","packages/bruno-electron/**","packages/bruno-cli/**","packages/bruno-lang/**","packages/bruno-filestore/**","packages/bruno-schema/**","packages/bruno-schema-types/**","packages/bruno-converters/**"]},"isInternal":false,"tokens":3170,"sizeBytes":12882},{"name":"electron-ipc.md","path":".claude/rules/electron-ipc.md","rawUrl":"https://raw.githubusercontent.com/usebruno/bruno/HEAD/.claude/rules/electron-ipc.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"---\npaths:\n  - \"packages/bruno-electron/**/*\"\n---\n\n# Electron Main Process Guide\n\nThe main process (`packages/bruno-electron/src/index.js`) owns the filesystem, network, child\nprocesses, and all electron-store data. The renderer reaches it **only** over IPC through the\npreload bridge — there is no direct Node access in the renderer despite `nodeIntegration: true`\n(see Security below). Before adding or changing any IPC, read this file plus the target\n`src/ipc/<domain>.js` handler.\n\n## Preload bridge & renderer access\n\n`src/preload.js` is the *only* surface the renderer sees. It exposes, via\n`contextBridge.exposeInMainWorld('ipcRenderer', …)`, exactly:\n\n- `invoke(channel, ...args)` → `ipcRenderer.invoke` (renderer→main request/response)\n- `send(channel, ...args)` → `ipcRenderer.send` (fire-and-forget)\n- `on(channel, handler)` → subscribe; **returns an unsubscribe function** and **strips the\n  Electron `event`/`sender` arg** — renderer handlers receive `(...args)`, not `(event, ...args)`\n- `getFilePath(file)` (via `webUtils.getPathForFile`) and `openExternal(url)` (via `shell.openExternal`)\n\nAlso exposed: `window.isPlaywright` (`process.env.PLAYWRIGHT === 'true'`).\n\nRenderer code accesses this as `window.ipcRenderer` (e.g. `const { ipcRenderer } = window;` in\n`bruno-app/src/providers/App/useIpcEvents.js`). **There is no channel allowlist** — any channel\nstring is forwarded. Do not rely on the preload to gate channels; validate inputs inside the\nhandler.\n\n## Channel naming convention\n\nFollow the prefix that matches the direction — the vast majority of handlers already do:\n\n- **`renderer:*`** — renderer→main via `ipcMain.handle` (the default for new work; ~255 of ~290\n  handlers). e.g. `renderer:ready`, `renderer:save-request`, `renderer:create-workspace`,\n  `renderer:browse-directory`, `renderer:get-global-environments`.\n- **`main:*`** — main→renderer push via `webContents.send`. e.g. `main:load-preferences`,\n  `main:collection-opened`, `main:workspace-opened`, `main:collection-tree-updated`,\n  `main:display-error`, `main:app-loaded`. A few `main:*` names are *internal* main-process\n  events routed through `ipcMain.emit`/`ipcMain.on` (`main:renderer-ready`,\n  `main:workspaces-ready`) — see the startup sequence.\n- **Domain-namespaced invokes** for stateful sub-protocols: `grpc:*`, `renderer:ws:*` (WebSocket),\n  `terminal:*`, `menu:*` (menu→main), `internal:*` (main-to-main only).\n- **Legacy unprefixed network channels** — `send-http-request`, `cancel-http-request`,\n  `fetch-gql-schema`, `clear-oauth2-cache`, `connections-changed`. These predate the convention;\n  prefer a `renderer:*` name for new request-side channels rather than adding to this unprefixed set.\n\n## Handler registration\n\nAll handlers are registered inside `app.on('ready')` in `index.js` (the `// register all ipc\nhandlers` block) by calling per-domain `register*Ipc(mainWindow, …)` functions, each `require`d\nat the top of `index.js`. `registerPreferencesIpc` (which kicks off onboarding + the\n`renderer:ready` handler) runs *before* `registerWorkspaceIpc` (which owns `main:renderer-ready`);\nthis is safe because `renderer:ready` only fires after the renderer mounts, by which point every\nhandler is registered. **A new IPC domain must add its own `register*Ipc` call here** — a handler\nfile that is never `require`d + called registers nothing.\n\n## Where new handler code goes — keep it small and testable\n\n**Don't let a handler file keep growing to where its size hurts readability and maintainability.**\nWhen a group of handlers forms a separable feature, give it its own `ipc/<domain>.js` with its own\n`register*Ipc` (wired into `index.js` as above) rather than appending to an existing large file —\nseparate it whenever it makes sense. Splitting by domain is already the norm here (`apiSpec.js`,\n`workspace.js`, `mount.js`, …).\n\n**Structure a handler so its callback is unit-testable.** Write the logic as a named, exported\nfunction and let the thin `ipcMain.handle(channel, fn)` line just call it — not an inline anonymous\narrow, which can only be exercised through Electron. The exported callback can then be tested\nwithout spinning up the main process: see `ipc/swagger-fetch.js` (`proxySwaggerFetch`) and the\nspecs under `ipc/network/` and `ipc/ai/`.\n\n## Error-handling convention\n\nTwo patterns by handler type — match the one that fits:\n\n- **CRUD / lifecycle handlers throw or reject.** Validation failures use bare\n  `throw new Error(...)`; caught errors are re-surfaced with `return Promise.reject(error)` or\n  `throw error`. The renderer `invoke` promise rejects and is shown via a `main:display-error`\n  toast (`useIpcEvents.js`). A few read-only probes intentionally swallow and return a fallback\n  (e.g. `exists-sync` → `false`, `get-collection-workspaces` → `[]`).\n- **Request-execution handlers embed the error in the result.** `send-http-request`\n  (`ipc/network/index.js`) does **not** reject on a failed request/script/test — it returns a\n  result object carrying an `error` field so the renderer can render the failure as data. New\n  network handlers follow this embed-in-result shape; new CRUD handlers throw/reject.\n\n## Security constraints\n\n`BrowserWindow` `webPreferences` (in `index.js`): `nodeIntegration: true`,\n`contextIsolation: true`, `preload: preload.js`, `webviewTag: true`. **This `nodeIntegration:\ntrue` + `contextIsolation: true` combination is intentional — do not \"fix\" it.** No `sandbox`\nkey (defaults off); `webSecurity` defaults on. A CSP is set via `electron-util`'s\n`setContentSecurityPolicy` in `index.js` (`default-src 'self'`; `connect-src` adds PostHog;\n`script-src 'self' data:`; `img-src` allows http/https/blob/data). `form-action 'none'` is\ndeliberately commented out to keep OAuth2 working. External navigation is hardened via\n`will-redirect` + `setWindowOpenHandler`, which route http/https to `shell.openExternal` and deny\neverything else — preserve this when touching window/navigation code.\n\n## IPC startup sequence\n\n```\nrenderer mounts\n  -> useEffect: ipcRenderer.invoke('renderer:ready')    [useIpcEvents.js]\n     -> main handles 'renderer:ready'                    [ipc/preferences.js]\n          await onboardingPromise\n          send main:load-preferences\n          send main:load-global-environments (+ main:git-version, reopen last apispecs, ...)\n          ipcMain.emit('main:renderer-ready')\n            -> workspace.js listener:                     [ipc/workspace.js]\n                 await ensureDefaultWorkspaceExists()\n                 send main:workspace-opened (per workspace)\n                 send + ipcMain.emit('main:workspaces-ready')\n                   -> onboarding.js sends main:collection-opened  [app/onboarding.js]\n\ndid-finish-load (independent, from Chromium):                     [index.js]\n  -> wraps webContents.send with a safeStringify/safeParse JSON roundtrip\n  -> send main:app-loaded\n     -> renderer sink sets data-app-state=\"loaded\"        [pages/Bruno/index.js, NOT useIpcEvents.js]\n```\n\n## IPC race-condition pattern\n\nWhen `ipcMain.emit` triggers multiple `ipcMain.on` listeners:\n- Sync listeners complete fully before the next starts.\n- Async listeners yield at the first `await`, letting the next listener start.\n- `webContents.send` from earlier listeners can arrive at the renderer before messages from\n  later async continuations.\n\nFix: chained events (`main:renderer-ready` → `main:workspaces-ready` → deferred\n`main:collection-opened`) enforce ordering. Reuse this pattern rather than adding `setTimeout`s.\n\n## Persistent stores (`src/store/`, electron-store)\n\nStores are electron-store JSON files under `app.getPath('userData')`, one module per file —\n**read the directory for the current set** (it is large: preferences, cookies, oauth2,\ncollection-security, secrets, workspace/global environments, last-opened-*, window-state, …).\nGotcha: multiple modules can share the *same* JSON file. `lastOpenedCollections` lives in\n`preferences.json` but is owned by `store/last-opened-collections.js` (a distinct\n`new Store({ name: 'preferences' })` module), **not** by `store/preferences.js`. Don't assume a\nstore key belongs to the module named after the file.\n\n## File watchers (`src/app/`)\n\nWatchers live in `src/app/` — **read the directory for the current set** (`collection-watcher.js`,\n`workspace-watcher.js`, `apiSpecsWatcher.js`, `dotenv-watcher.js`). Cleanup is\n**not** uniform:\n- `closeAllWatchers()` in `index.js` tears down exactly three —\n  `collectionWatcher`, `workspaceWatcher`, `apiSpecWatcher` — each of which exposes\n  its own `closeAllWatchers()`.\n- `dotenv-watcher.js` is a shared singleton exposing `closeAll()` and is torn down transitively by\n  the workspace watcher (and driven by the collection watcher), **not** from `index.js`.\n\nWhen adding a watcher, decide which lifecycle it belongs to and register its cleanup in the\nmatching place — a top-level watcher goes into `closeAllWatchers()` in `index.js`; a\nper-workspace/per-collection one hangs off the owning watcher.\n\n## Onboarding flow\n\nEntry: `src/app/onboarding.js` (`onboardUser`), invoked synchronously when `registerPreferencesIpc`\nruns.\n\n1. Checks `preferencesUtil.hasLaunchedBefore()`.\n2. New users: imports the sample collection from `resources/data/sample-collection.json`.\n3. Defers `main:collection-opened` (via a pending-collection var) until `main:workspaces-ready`.\n4. `DISABLE_SAMPLE_COLLECTION_IMPORT === 'true'` skips the import (set in Playwright).\n\n## Key environment variables\n\n- `ELECTRON_USER_DATA_PATH` — custom userData path, **dev mode only** (`index.js:22`, guarded by `isDev`).\n- `DISABLE_SAMPLE_COLLECTION_IMPORT` — skip sample collection on first launch.\n- `PLAYWRIGHT` — signals the app is running under Playwright (read in `preload.js`, stores, etc.).\n- `DISABLE_SINGLE_INSTANCE` — allow multiple app instances.\n\n(All four are set to `'true'` for e2e in `playwright/index.ts`.)\n\n## Before adding or changing an IPC handler — checklist\n\n- [ ] Handler added in `src/ipc/<domain>.js` as `ipcMain.handle('renderer:<verb>', …)` (or the\n      matching domain prefix); no new unprefixed channel.\n- [ ] New handler lives in a focused `ipc/<domain>.js` — a separable feature gets its own file\n      rather than bloating an existing one; its callback is a named exported function\n      (unit-testable), not an inline arrow.\n- [ ] If it's a new domain, its `register*Ipc(mainWindow, …)` is called in `index.js`'s ready block.\n- [ ] Inputs validated inside the handler (the preload has no allowlist).\n- [ ] Error path matches the handler type — CRUD throws/rejects; request-execution embeds `error`\n      in the result.\n- [ ] For a `main:*` push channel, the renderer adds a listener **and** an unsubscribe in\n      `useIpcEvents.js`'s cleanup.\n- [ ] Cross-process values survive the `did-finish-load` JSON roundtrip (no functions,\n      class instances, or circular refs in payloads).\n","frontmatter":{"paths":["packages/bruno-electron/**/*"]},"isInternal":false,"tokens":2656,"sizeBytes":10982},{"name":"feature-parity.md","path":".claude/rules/feature-parity.md","rawUrl":"https://raw.githubusercontent.com/usebruno/bruno/HEAD/.claude/rules/feature-parity.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"---\npaths:\n  - \"packages/**/*\"\n---\n\n# Co-Related Features & Parity Surfaces\n\nBruno implements the same behaviour more than once — app vs CLI, `.bru` vs `.yml`, filestore vs\nconverters, implementation vs QuickJS shim. **A change to one side of a pair is not a finished\nchange**: the other side silently keeps the old behaviour and ships. The symptom is never a broken\nbuild — it's a feature that works in the app but not in `bru run`, a field that survives save but\nvanishes on export or migration, a `bru.*` method that is `undefined` in the default sandbox.\n\nThis rule maps *which* surfaces pair up. `.claude/rules/dsl-changes.md` owns the on-disk contract\nin depth.\n\n## When you work on a change\n\n1. **Find the twin before writing the fix.** For every file you touch in the tables below, open\n   its counterpart.\n2. **Update the twin in the same change** when it's within what was asked.\n3. **Otherwise, say so explicitly.** If the twin is out of scope, doesn't apply, or you're unsure,\n   list it in your reply to the user (and in the PR description) with the path and why it was left\n   — never leave a one-sided change unmentioned.\n4. **Prefer unifying over copying.** If the logic has no Electron- or CLI-only dependency, move it\n   into `@usebruno/requests` / `@usebruno/common` (respecting `.claude/rules/architecture.md`)\n   instead of patching twice. Precedent: `utils/cookies.js` is one line in each runtime —\n   `module.exports = require('@usebruno/requests').cookies;`. Where both copies must stay, note the\n   pairing in a comment at both sites.\n\n## 1. App ↔ CLI — highest priority\n\n`bruno-cli` is how collections run in CI. Several pipeline modules are **copied, not shared**, and\na bug fixed in the app is still present in `bru run` until its twin is fixed. Trace app-reported\nbugs to the shared logic, not the reporting surface.\n\n| Concern | `bruno-electron/src/…` | `bruno-cli/src/…` |\n|---|---|---|\n| Request execution — pre/post scripts, vars, asserts, tests, runner flow (`setNextRequest`, `skipRequest`) | `ipc/network/index.js` | `runner/run-single-request.js`, `commands/run.js` |\n| Request assembly | `ipc/network/prepare-request.js` | `runner/prepare-request.js` |\n| HTTP client, TLS, certs | `ipc/network/axios-instance.js` | `utils/axios-instance.js` |\n| Interpolation | `ipc/network/interpolate-{vars,string}.js` | `runner/interpolate-{vars,string}.js` |\n| Collection tree, `merge{Headers,Scripts,Vars,Auth}` | `utils/collection.js` | `utils/collection.js` |\n| Collection loading, `bruno.json` / `opencollection.yml` config | `utils/collection-reader.js`, `app/collection-watcher.js` | `utils/collection.js` (`getCollectionConfig`) |\n| Environments & persisted vars | `utils/environments.js` | `utils/environment.js`, `utils/persist-variables.js` |\n| Proxy · OAuth 2.0 · form-data · filesystem | `utils/{proxy-util,oauth2,form-data,filesystem}.js` | `utils/{proxy-util,oauth2,form-data,filesystem}.js` |\n| AWS SigV4 | `ipc/network/awsv4auth-helper.js` | `runner/awsv4auth-helper.js` |\n\n- A new app runner option (tags, delay, bail) needs a `bru run` flag, or a stated reason.\n- Import: the app imports Postman, Insomnia, OpenAPI, WSDL and OpenCollection; `bru import`\n  (`commands/import.js`) only OpenAPI and WSDL. Changes to a shared converter affect both.\n- **Known-divergent by design.** The CLI has no preferences store (so no `resolveInheritedSettings`),\n  no Electron window, no system-browser OAuth, and no `@usebruno/schema` validation. Its runner\n  supports only `http-request` and `graphql-request`: it has no gRPC or WebSocket transport, and\n  every request item goes through the HTTP pipeline in `runner/run-single-request.js`.\n\n## 2. Persisted fields — every layer that carries a field list\n\nA field is added in one place and read in many. Each of these keeps its **own** list, aligned by\nhand:\n\n| Layer | Where | Missed → |\n|---|---|---|\n| Types | `bruno-schema-types/src/collection/item.ts` | type drift |\n| Yup schema | `bruno-schema/src/collections/index.js` — `.noUnknown(true).strict()` | **throws on save** |\n| App save transform | `bruno-app/src/utils/collections/index.js` — `transformRequestToSaveToFilesystem`, `transform{Collection,Folder}RootToSave`, `transformCollectionToSaveToExportAsFile` | dropped before reaching disk / Bruno JSON export |\n| On-disk formats | `bruno-filestore/src/formats/{bru,yml}` (+ `bruno-lang`) — parse **and** stringify | lost in one format |\n| BRU → YML migration | `bruno-electron/src/ipc/yml-migration.js` (bru parse → yml stringify) | lost when a user migrates |\n| Collection config | `bruno.json` ↔ `opencollection.yml` — `utils/transformBrunoConfig.js`, `stringifyCollection`, migration's config transform (e.g. `transformProxyConfig`), CLI `getCollectionConfig` | config lost or misread by CLI |\n| OpenCollection import/export | `bruno-converters/src/opencollection/items/{http,graphql,grpc,websocket}.ts`, `folder.ts`, `environment.ts` — **both** directions; app entry `bruno-app/src/utils/exporters/opencollection.js` | lost on export or import |\n| Other converters | `postman/{postman-to-bruno,bruno-to-postman}.js`, `insomnia/`, `openapi/`, `wsdl/`; app exporters `postman-collection.js`, `openapi-spec.js`, `bruno-environment.js` | lost on import/export where the target can represent it |\n\nThe migration also rewrites content, not just shape: `bru.runRequest(\"x.bru\")` paths are stripped\nof `.bru`, and tab snapshots are remapped. A new script API that takes a file path, a new file\ntype, or a new `bruno.json` key needs a matching migration step.\n\n## 3. Scripting API ↔ QuickJS shim\n\n`bruno-js/src/{bru,bruno-request,bruno-response}.js` implement the API. The **QuickJS sandbox — the\ndefault \"safe\" mode — bridges each method explicitly** in `bruno-js/src/sandbox/quickjs/shims/`\n(`bru.js`, `bruno-request.js`, `bruno-response.js`, `bruno-grpc.js`, …). Implementation only →\nworks in developer mode, `undefined` for most users.\n\n## 4. Scope levels — collection ↔ folder ↔ request\n\nAuth, headers, vars, scripts and tests exist at three levels plus `inherit`. Each level has its own\nUI (`CollectionSettings/`, `FolderSettings/`, `RequestPane/`), and resolution lives in the merge\nhelpers in **each** runtime's `utils/collection.js`. A feature at one level needs a stated decision\nfor the other two and for `inherit`.\n\n## 5. Protocols — HTTP ↔ GraphQL ↔ gRPC ↔ WebSocket\n\n`REQUEST_TYPES` (`bruno-app/src/utils/common/constants.js`, `bruno-electron/src/utils/constants.js`)\nlists all four. Each has its own `parse*`/`stringify*` in `bruno-filestore/src/formats/yml/items/`\nand its own converter in `bruno-converters/src/opencollection/items/`. \"HTTP only\" is a legitimate\nscope — leaving it unstated is how a gap becomes a bug report. Widening CLI protocol support is a\ndeliberate decision, not an incidental one.\n\n## 6. Request shaping ↔ code generation\n\n`GenerateCodeItem/utils/snippet-generator.js` consumes headers, body, `settings` and resolved auth.\nA new auth mode or a setting that changes the wire request should change the generated snippet.\n\n## Parity checklist\n\n- [ ] Every twin in §1 opened; updated, or listed to the user with a reason\n- [ ] App-reported bug fixes verified on the CLI path\n- [ ] Shared logic moved to a leaf package where the DAG allows, instead of patched twice\n- [ ] Persisted field carried through every layer in §2 — including migration and converters (both directions)\n- [ ] New/changed `bru.*`, `req.*`, `res.*` API bridged in the QuickJS shim\n- [ ] Collection / folder / request levels and `inherit` each decided\n- [ ] gRPC and WebSocket considered; any \"HTTP only\" scope stated\n- [ ] Code generation updated if the wire request changed\n- [ ] Tests on **both** sides of every pair touched\n\nHuman-facing version of this map (for QA): Confluence *Related Feature Map — Change Impact &\nParity Checklist*, https://usebruno.atlassian.net/wiki/spaces/Bruno/pages/1376944136. Keep the two\nin step: add new pairings to both, and remove unified ones from both.\n","frontmatter":{"paths":["packages/**/*"]},"isInternal":false,"tokens":2077,"sizeBytes":8060},{"name":"redux-store.md","path":".claude/rules/redux-store.md","rawUrl":"https://raw.githubusercontent.com/usebruno/bruno/HEAD/.claude/rules/redux-store.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"---\npaths:\n  - \"packages/bruno-app/**/*\"\n---\n\n# React / Redux Architecture\n\n## Store configuration\n\n`configureStore` (Redux Toolkit **v1.8**, not v2) is set up in\n`src/providers/ReduxStore/index.js`. That file's `reducer` map is the **authoritative slice list**\n(~11 keys); read it rather than trusting any list here. Middleware is\n`getDefaultMiddleware().concat([...])` with **no options passed**, so RTK's defaults —\n`thunk`, plus `serializableCheck` and `immutableCheck` in dev — are all active.\n\n**Consequences to respect:**\n- **State must stay serializable.** Don't put functions, class instances, Promises, or DOM nodes\n  in the store — dev `serializableCheck` will flag them. Do **not** disable `serializableCheck` in\n  the real store to work around it (the only place it's disabled is a middleware unit test).\n- **Reducers mutate the Immer draft directly.** `createSlice` reducers write\n  `state.collections.push(...)`, `item.name = ...`, `.splice(...)` — do **not** spread-clone or\n  return new objects unless replacing wholesale. Match the surrounding reducer.\n\n## Slice organization\n\nSlices live in `src/providers/ReduxStore/slices/`, one domain each; `collections/` is by far the\nlargest (collection tree, requests, environments, runner, drafts — ~200 reducer action creators in\n`collections/index.js`, plus ~150 thunks in `collections/actions.js`, and more spread in from\n`collections/exampleReducers.js`). Read the specific slice for its real state and actions.\n\nTwo gotchas:\n- **Reducer key ≠ folder/file name.** e.g. `globalEnvironments` comes from\n  `slices/global-environments.js`; `openapiSync` from `slices/openapi-sync.js`.\n- **Action-type prefix follows the slice's `name:`, not the reducer key.** e.g.\n  `global-environments/…`, `collections/…`, `app/…`. Grep the action type, not the key, when\n  tracing dispatches.\n\n## Actions / thunks convention\n\nAsync logic is written as plain hand-written thunks — a function that returns\n`(dispatch, getState) => …`. `createAsyncThunk` isn't used anywhere today:\n\n```javascript\nexport const saveRequest = (itemUid, collectionUid) => (dispatch, getState) => { ... };\n// or async\nexport const openCollectionEvent = (uid, pathname, ...) => async (dispatch, getState) => { ... };\n```\n\nThunks live in the slice's `actions.js` (`slices/collections/actions.js` is the big one:\n`sendRequest`, `saveRequest`, `importCollection`, `pasteItem`, `deleteItem`, `openCollectionEvent`\n— the last handles the `main:collection-opened` IPC). Reducers (in the slice's `index.js`) stay\nsynchronous. Follow the plain-thunk shape by default so new code matches the rest — but this is a\nconvention, not a blocker: if a feature genuinely benefits from `createAsyncThunk` (e.g. its\nbuilt-in pending/fulfilled/rejected states), it's fine to reach for it deliberately.\n\n## Side-effect boundaries\n\n- **Reducers are pure** — no IPC, no network, no `Date.now`/random, no async. Immer draft\n  mutations only.\n- **Side effects live in thunks and middleware.** IPC is invoked from thunks via\n  `window.ipcRenderer` (e.g. `saveRequest` → `ipcRenderer.invoke('renderer:save-request', …)`); the\n  `collections/actions.js` file alone has ~200 `ipcRenderer` calls.\n- **The file-IO → open-tab pattern** is split across a thunk and the `tasks` middleware: a thunk\n  queues an `OPEN_REQUEST`/`OPEN_EXAMPLE` task (`insertTaskIntoQueue`) and fires the IPC write; the\n  `tasks` middleware watches for the resulting `main:*` file event, then dispatches `addTab` +\n  `removeTaskFromQueue`. Reuse this queue rather than opening the tab optimistically.\n\n## Custom middleware (`middlewares/`)\n\nFive middlewares, registered in `index.js` (read the directory + registration array for the live\nset):\n- **`tasks`** — listener middleware; opens a request/example tab after the file event for a queued\n  task arrives (see file-IO pattern above).\n- **`draft`** — on the first edit action, promotes a *preview* tab to permanent\n  (`handleMakeTabParmanent`). It does **not** write draft state — the reducers do.\n- **`autosave`** — debounced by `preferences.autoSave.interval`, gated on\n  `app.preferences.autoSave.enabled`; dispatches the `save*` thunks.\n- **`snapshot`** — debounced (~1s), gated on `app.snapshotReady`; persists a curated state subset\n  to disk (see Persistence).\n- **`debug`** (dev only) — logs every action; added only when `import.meta.env.MODE ===\n  'development'`.\n\n## Persistence / serialization constraints\n\nThe store isn't persisted wholesale, and there's no `redux-persist`. Instead the `snapshot`\nmiddleware serializes a **curated subset** — workspaces, collection metadata, per-collection tabs,\nselected environments, and devtools console open-state (`extras.devTools`, from the `logs` slice;\nsee `middlewares/snapshot/serializeSnapshot.js`) — and writes it to disk via IPC. Request/response\nbodies are deliberately not persisted. To persist a new piece of state, extend `serializeSnapshot`\nand its save-trigger list (`utils/snapshot`) — the snapshot is the established mechanism, so prefer\nit over adding `redux-persist` or `localStorage`.\n\n## Selectors\n\nInline `useSelector` in components is the norm (hundreds of usages); memoized `createSelector` is\nused in just one place (`src/selectors/tab.js`). Default to inline\n`useSelector`, and reach for `createSelector` when a derivation is genuinely expensive or reused.\n\n## What belongs in Redux (and what doesn't)\n\n- **In Redux:** draft / unsaved state (`item.draft`, `collection.draft`, `environmentsDraft`,\n  `globalEnvironmentDraft`), and cross-component or persisted UI state (`sidebarCollapsed`,\n  `leftSidebarWidth`, `taskQueue`, `item.collapsed`, active tab).\n- **Local `useState`:** ephemeral, component-scoped UI — search text, inline-create toggles, hover,\n  transient form state. Don't lift these into a slice.\n\n## Sidebar collection visibility\n\nCollections render in the sidebar only when they belong to the **active workspace**.\n`Sidebar/Collections/index.js` builds the list in a `useMemo` that walks\n`activeWorkspace.collections`, matches loaded collections through a `Map` keyed on\n`normalizePath(pathname)`, **excludes scratch collections** (`isScratchCollection`), adds \"ghost\"\ngit-remote rows (`GitRemoteCollectionRow`) for non-default workspaces, and sorts alphabetically.\nRead the current `useMemo` before changing this. Collapsed folders do **not** render their children\nin the DOM (conditional render on `item.collapsed`).\n\n## Sidebar DOM structure (for Playwright)\n\n`.collection-item-name` is the class on the **row-wrapper** div for every item (folders, requests,\nJS files) in `Sidebar/Collections/Collection/CollectionItem/index.js` — the wrapper also contains\nthe name span and `.menu-icon`. Use it for Playwright locators.\n\n## Providers (`src/providers/`)\n\nRead the directory for the current set: `App/` (IPC listeners in `useIpcEvents.js`, app init),\n`ReduxStore/`, `Theme/`, `Hotkeys/`, `Toaster/`, `PromptVariables/`.\n\n## Checks after changing a slice / middleware\n\nSlices and middleware have Jest coverage under `src/providers/ReduxStore/` (e.g.\n`slices/collections/*.spec.js`, `slices/global-environments.spec.js`,\n`middlewares/snapshot/serializeSnapshot.spec.js`). After a change, run the relevant spec:\n\n```bash\nnpm test --workspace=packages/bruno-app -- <path-or-pattern>\n```\n\nIf you touch the reducer map, thunks that fire IPC, or the snapshot subset, also sanity-check that\nstate stays serializable (no dev `serializableCheck` warnings) and that persisted shape still\nround-trips through `serializeSnapshot`.\n","frontmatter":{"paths":["packages/bruno-app/**/*"]},"isInternal":false,"tokens":1832,"sizeBytes":7586},{"name":"sqlite.md","path":".claude/rules/sqlite.md","rawUrl":"https://raw.githubusercontent.com/usebruno/bruno/HEAD/.claude/rules/sqlite.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"---\npaths:\n  - \"packages/bruno-sqlite/**/*\"\n  - \"packages/bruno-electron/src/ipc/sqlite.js\"\n  - \"packages/bruno-electron/src/services/sqlite/**/*\"\n---\n\n# SQLite SDK (`@usebruno/sqlite`)\n\nBruno's local database layer. You author **migrations** (`migrations/*.ts`) and **statements**\n(`statements/*.sql`); a codegen step compiles them into a typed data layer for the Electron main\nprocess, which owns the database and executes statements. The package has no renderer entry point:\nthe renderer reaches a statement only through an IPC handler written by hand for that statement in\n`bruno-electron`.\n\n**The renderer never sends SQL, and never picks the statement.** Each IPC channel is bound to exactly\none statement in the main process; the renderer sends only that statement's params. Do not add an\nIPC handler, statement, or option that accepts SQL text or a statement name from the renderer.\n\n## When to use it — and when not to\n\nUse it for state that is **derived, bulky, or secondary**: data Bruno can rebuild or lose without\ncorrupting anything the user authored, and that would bloat Redux or the IPC payloads if held in\nmemory.\n\nIt is **not** the right home for:\n\n- **Anything the user authored.** Collections, requests, environments, folders and `bruno.json` live\n  on disk as `.bru`/`.yml` — see `.claude/rules/dsl-changes.md`. Never mirror or migrate a\n  serialized field into SQLite.\n- **App preferences and small keyed state** — those are electron-store JSON files under\n  `src/store/` (see `.claude/rules/electron-ipc.md`).\n- **UI state** — Redux, or local `useState` (see `.claude/rules/redux-store.md`).\n\nThe database file is `bruno.db` under `app.getPath('userData')`, and the file store spills large\npayloads to `sqlite-files/` beside it. **Treat both as disposable**: the open path below deletes\nand rebuilds the database rather than failing (startup `collect()` then sweeps the spilled files no\nrow names), so never let either hold the only copy of anything.\n\n## Package layout\n\n```\npackages/bruno-sqlite/\n  migrations/<seq>_<name>.ts   authored — exports up()/down() returning SQL\n  statements/*.sql             authored — sqlc-style annotated statements\n  scripts/                     codegen (generate-artifacts), new-migration, verify-migrations,\n                               generate-schema\n  SCHEMA.md                    GENERATED, committed — ER diagram + table schemas\n  src/node/                    db.ts (migrations), statements.ts (prepare/execute)\n  src/shared/                  shared types\n  src/generated/               GENERATED, gitignored — never edit or commit\n  tests/node\n```\n\n**`@usebruno/sqlite` (= `/node`) is main-process only** (it imports `node:sqlite`, `node:fs`,\n`node:crypto`); never import it from the renderer. The package has zero internal `@usebruno/*`\ndependencies and must stay that way.\n\n`src/generated/` is produced from the authored migration and `.sql` files by `npm run generate`\n(which `prebuild`/`pretest` run for you). Those authored files are the only source of truth.\n\n## Adding a migration\n\n```bash\nnpm run migration:new --workspace=packages/bruno-sqlite    # prompts for a name\n```\n\nThis scaffolds `migrations/<7-digit sequence>_<name>.ts` exporting `up` and `down`, each returning\nthe SQL to apply or revert:\n\n```ts\nexport const up = (): string => {\n  return 'CREATE TABLE users (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, email TEXT);';\n}\nexport const down = (): string => {\n  return 'DROP TABLE users;';\n}\n```\n\nMigrations run at open time, in sequence order, each in its own transaction, and are recorded in a\n`_migrations` table with a sha256 of both `up` and `down`.\n\n- **A merged migration is immutable.** Editing one changes its hash; the next open throws\n  `does not match the migration already applied`, and `createDatabase` responds by **deleting the\n  user's database and rebuilding it**. Add a new migration instead — always.\n- **`down` must genuinely revert `up`.** It is not decoration: when the tail migration disappears\n  from the code (a user downgrading Bruno, or you switching branches), the recorded `down` is\n  executed automatically against the existing database. An empty or wrong `down` corrupts that path.\n- Only the **tail** rolls back (recorded sequences above the highest local one). Deleting a\n  migration from the middle silently leaves its schema in place — don't.\n- Design schema changes to tolerate an older Bruno opening the same file afterwards: prefer new\n  tables and nullable columns over reshaping an existing one.\n\nVerify against a real database rather than an empty one — this replays every migration on a\nthrowaway `VACUUM INTO` copy and then prepares every statement against the result:\n\n```bash\ncp .env.example .env    # set DB_PATH to an absolute path to a real bruno.db\nnpm run migration:verify --workspace=packages/bruno-sqlite\n```\n\n`SCHEMA.md` documents the resulting schema: a Mermaid ER diagram plus each table's columns, keys and\nindexes, produced by applying every migration to an empty in-memory database. The pre-commit hook\nregenerates and stages it whenever a file under `migrations/` is staged; run\n`npm run schema --workspace=packages/bruno-sqlite` to regenerate it by hand. Never edit it directly.\n\n## Adding statements\n\nStatements live in `statements/*.sql`, several per file, each introduced by a sqlc-style\nannotation. Params are **named** and always passed as an object — there is no positional binding:\n\n```sql\n-- name: list_users :many\nSELECT id, name, email FROM users ORDER BY name;\n\n-- name: get_user :one\nSELECT id, name, email FROM users WHERE id = @id;\n\n-- name: create_user :exec\nINSERT INTO users (name, email) VALUES (@name, @email);\n```\n\n| annotation | runs as | returns |\n|---|---|---|\n| `:one` | single row | row or `undefined` |\n| `:many` | rows | array |\n| `:exec` | write | `{ changes, lastInsertRowid }` |\n\n- Statement names are **globally unique across all `.sql` files** (a duplicate fails codegen).\n  Renaming a statement, or moving it to another file, means renaming its IPC channel and every\n  renderer call site too.\n- Use `@param` for consistency with the existing statements, even though `node:sqlite` also accepts\n  `:name` and `$name`.\n\n## Main process\n\n`packages/bruno-electron/src/services/sqlite/index.js` owns the single database instance:\n`openDatabase()` calls `createDatabase` on `bruno.db` and starts a background `files.collect()`; the\nservice exposes `getStatements()`, `getFiles()` (the SDK's file store — see the package README),\n`transaction(callback)`, `reclaimDiskSpace()` and `shutdown()`. It never hands out the `DB` itself,\nand it knows nothing about IPC. `index.js`'s ready block opens it (`sqliteService.openDatabase()`),\nand `before-quit` reclaims disk space and shuts it down.\n\n```js\nconst { getStatements, transaction } = require('../../services/sqlite');\n\ntry {\n  transaction(() => {\n    getStatements().execute('create_user', { name, email });\n  });\n} catch (err) {\n  // degrade: the database may be unavailable\n}\n```\n\n`getStatements()` never returns `null`. Before `openDatabase()`, after shutdown, and when\n`createDatabase` returns `{ db: undefined, statements: undefined }` (even the in-memory fallback\nfailed), it returns a stub whose `execute` logs the skipped statement and throws; `getFiles()`\nreturns a matching stub whose methods log and throw (or reject, for the async ones), and\n`transaction` throws the same way. So there is one failure mode, and every caller must handle it:\n\n- **`execute` and `transaction` throw** when the database is unavailable, for an unknown statement,\n  for one that couldn't be prepared against this schema (prepare failures are logged at construction\n  and only that statement is discarded — the rest of the database stays usable), and for any SQL\n  error. Wrap calls in try/catch and stay useful without the database — degrade, don't crash.\n\n`createDatabase` degrades rather than failing: file → (migration error) delete the `bruno.db*` files\nand rebuild → in-memory → `undefined`. A real backup is still a `TODO` in `src/node/index.ts`; it\ndeletes today.\n\n**`node:sqlite` is synchronous — every `execute` blocks the main process.** Keep statements indexed,\nbounded, and off hot paths; no full-table scans, no unbounded result sets, no per-event writes in a\ntight loop.\n\n## Exposing a statement to the renderer\n\nOnly statements the renderer actually needs are exposed, each with its own hand-written\n`ipcMain.handle` in `packages/bruno-electron/src/ipc/sqlite.js`. `registerSqliteIpc()` is called in\n`index.js` right after `openDatabase()`; each handler calls the service's `getStatements()` per\ninvocation, so a call after shutdown hits the stub, not a closed database.\n\nThe channel name follows a fixed convention: **`datastore:<statement_file>:<statement_name>`**, where\n`<statement_file>` is the `.sql` file's name without the extension, written exactly as the file is\nnamed. Channel names are written by hand, not generated.\n\n```js\nipcMain.handle('datastore:runner_responses:get_runner_response', (_event, params) => {\n  const request_uid = requireUid(params?.request_uid, 'request_uid');\n  return getStatements().execute('get_runner_response', { request_uid });\n});\n```\n\n- The handler validates and picks the params it forwards — never pass the renderer's object\n  straight through to `execute`.\n- The file store has no channels of its own. File ids are sequential and unscoped, so a channel\n  that takes one would expose every stored blob; the renderer reaches files only through a\n  domain handler that resolves them from a scoped key (e.g. `get_runner_response` by\n  `request_uid`). File writes stay main-process only — the renderer never chooses what gets\n  stored.\n- In the renderer, wrap each channel in a named hook or helper under `src/hooks/useX/index.js`\n  (or the relevant util) and call `window.ipcRenderer.invoke(channel, params)` there; no component\n  invokes a `datastore:*` channel directly.\n\n## Commands\n\n```bash\nnpm run build:bruno-sqlite                          # generate + rollup (from the repo root)\nnpm run watch --workspace=packages/bruno-sqlite     # rebuild on change\nnpm run test  --workspace=packages/bruno-sqlite     # jest; sets --experimental-sqlite for you\n```\n\n`npm run dev` does **not** rebuild this package, and bruno-electron consumes its `dist/` — after\nediting anything under `packages/bruno-sqlite`, rebuild or run the watcher or the app keeps the old\nstatements. Run the tests through the npm script: bare `jest` misses the\n`NODE_OPTIONS=\"--experimental-sqlite\"` the repo's Node needs for `node:sqlite`.\n\n## Before you call it done\n\n- [ ] New persistence genuinely belongs here — not `.bru`/`.yml`, electron-store, or Redux\n- [ ] No merged migration edited; new schema added as a new migration with a working `down`\n- [ ] `npm run generate` output shows the expected statements\n- [ ] `migration:verify` run against a real `bruno.db` copy\n- [ ] Main-process callers try/catch `execute` / `transaction`, and still work when the database\n      is unavailable\n- [ ] Each statement the renderer needs has its own `ipcMain.handle` on\n      `datastore:<statement_file>:<statement_name>`; renderer access goes through a named hook\n- [ ] Package rebuilt (`npm run build:bruno-sqlite`) before testing in the app\n- [ ] `npm run test --workspace=packages/bruno-sqlite` plus the affected app/electron specs\n","frontmatter":{"paths":["packages/bruno-sqlite/**/*","packages/bruno-electron/src/ipc/sqlite.js","packages/bruno-electron/src/services/sqlite/**/*"]},"isInternal":false,"tokens":2731,"sizeBytes":11415},{"name":"testing.md","path":".claude/rules/testing.md","rawUrl":"https://raw.githubusercontent.com/usebruno/bruno/HEAD/.claude/rules/testing.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"---\npaths:\n  - \"tests/**/*\"\n  - \"playwright/**/*\"\n  - \"playwright.config.ts\"\n---\n\n# Playwright E2E Testing Guide\n\n> **Reference:** For the full narrative guide — test structure, the page-module locator/action pattern, and worked examples — see [`docs/playwright-testing-guide.md`](../../docs/playwright-testing-guide.md). This file is the terse, path-scoped quick reference; the guide is the canonical source for conventions.\n\n## Running Tests\n\n```bash\nnpm run test:e2e                           # default + system-pac + mock-server (starts dev servers automatically)\nnpx playwright test tests/request/         # Run specific directory\nnpx playwright test --project=default      # Run specific project\nnpx playwright test --headed               # Watch mode\n```\n\nProjects: `default` (main), `system-pac` (depends on `default`), `mock-server`, `auth`, `ssl`.\n`npm run test:e2e` runs `default` + `system-pac` + `mock-server`; `auth` and `ssl` have their own\n`test:e2e:*` scripts and CI jobs. Config: `playwright.config.ts` — `fullyParallel: true`, `workers` unset\n(Playwright default, not single-worker), retries 0 local / 2 CI.\n\n## Test Fixtures (playwright/index.ts)\n\nSee `playwright/index.ts` for the current set; the table covers the common ones.\n\n| Fixture | Scope | Purpose |\n|---------|-------|---------|\n| `page` | test | Default page from `electronApp` |\n| `electronApp` | worker | Default Electron launch (skips onboarding) |\n| `launchElectronApp(opts)` | worker | Custom launch with userData, env, init data |\n| `reuseOrLaunchElectronApp(opts)` | worker | Caches app instances by key |\n| `pageWithUserData` | test | Loads `init-user-data/` from test dir, waits for app ready |\n| `createTmpDir(tag?)` | worker | Temp directory, auto-cleaned after worker |\n| `collectionFixturePath` | test | Copies `fixtures/collection(s)/` to temp dir |\n| `restartApp(opts)` | test | Restart with fresh user data |\n| `newPage` | test | Fresh app instance with tracing |\n| `installFakeClipboard(page)` | test | Replaces `navigator.clipboard` on that page for the test; `copiedText()` returns what the app copied |\n\n## Test Data Conventions\n\n- `tests/<suite>/init-user-data/` — Seed data for existing-user tests (`pageWithUserData`)\n- `tests/<suite>/init-user-data-fresh/` — Seed data for new-user tests (hasLaunchedBefore: false)\n- `tests/<suite>/fixtures/collection(s)/` — Collection fixtures auto-copied to temp dirs\n- `initUserDataPath` files support `{{projectRoot}}` template variable\n- No `initUserDataPath` → default prefs with `hasLaunchedBefore: true` (skip onboarding)\n- `dotEnv` overrides defaults (e.g., `DISABLE_SAMPLE_COLLECTION_IMPORT: 'false'`)\n- Adding/changing a key in the app's `preferences.json` defaults? Mirror it in the `defaultPreferences` mock in `playwright/index.ts` — see [the guide](../../docs/playwright-testing-guide.md#3-keep-defaultpreferences-in-sync-with-app-preferences).\n\n## Test Helpers (tests/utils/)\n\nReuse existing helpers instead of hand-rolling clicks; read the module for its current exports\n(e.g. `grep \"^export\" tests/utils/page/actions.ts`). Modules by responsibility:\n\n- `page/actions.ts` — high-level user actions (create/open collections, requests, folders, environments; send/save requests; workspace + tab management; import)\n- `page/locators.ts` — locator builders (`buildCommonLocators(page)` + per-protocol/feature variants) and table helpers\n- `page/runner.ts` — collection/folder runner actions and result assertions\n- `tests/utils/cli.ts` — `runCLI(cwd, args)` for bruno-cli testing\n- `tests/utils/wait.ts` — polling helpers (e.g. `waitForPredicate(predicate, {tries, interval})`)\n\n**Convention:** a spec must never inline raw selectors. Each `page/*` module owns one UI section and exports both a locator builder and its actions; a new section gets a new file whose builder is mapped into `buildCommonLocators`. Full pattern + examples in [the guide](../../docs/playwright-testing-guide.md#1-centralize-locators-and-actions-in-page-modules).\n\n## Waiting for App Ready\n\n```javascript\nawait page.locator('[data-app-state=\"loaded\"]').waitFor();\n```\n\nThis attribute is set when `main:app-loaded` IPC arrives (independent of `renderer:ready` chain).\n\n## Mutating Fixtures\n\nThe suite must be isolated (unique tmp paths, no shared state).\n\nMost specs load fixtures via `collectionFixturePath` (copies `fixtures/collection(s)/` to a temp dir) or `pageWithUserData` (loads `init-user-data/` into a fresh temp userData dir). These operate on **temp copies** — they need **no** cleanup, even though they mutate files on disk (e.g. persistence specs writing to `environments/*.bru` or `collection.bru`). Only a spec that mutates a **committed** fixture **in place** must restore it in `afterAll` — those use `git checkout <fixturePath>`. Don't flag a missing `afterAll` on a temp-copy spec.\n\n## Common Pitfalls\n\n1. **Worker-scoped fixtures persist across test retries** — app state from failed attempts carries over. If test creates resources (folders, collections), retries may fail trying to create duplicates.\n\n2. **Collapsed folders hide children from DOM** — `toHaveCount`/`toBeVisible` won't find items inside collapsed folders. Expand folders before asserting on their children.\n\n3. **Sequential tests sharing state** — Tests in the same `describe` that depend on prior test state will cascade-fail. The `afterAll` cleanup only runs once, not between retries.\n\n4. **File watcher timing** — After creating files via IPC, the collection watcher must detect and process the change before the UI updates. Use Playwright's auto-retrying assertions (e.g., `expect(locator).toBeVisible()`) rather than immediate checks.\n\n5. **Default env vars in fixture** — `launchElectronApp` sets `DISABLE_SAMPLE_COLLECTION_IMPORT: 'true'` by default. Tests that need onboarding must override via `dotEnv`.\n\n6. **The renderer outlives the test** — `electronApp` is worker-scoped and `page` hands back the same window, so anything a spec patches into the page (a global, a stubbed browser API) leaks into later specs in that worker. Use a fixture that restores on teardown; the real clipboard is also shared across workers, so assert copies through `installFakeClipboard(page)`, not `navigator.clipboard.readText()`.\n\n## Test Coverage Areas\n\nTest suites live under `tests/`, one directory per feature area — list that directory for the\ncurrent set. Examples: `request/`, `collection/`, `environments/`, `runner/`,\n`scripting/`, `graphql/`, `grpc/`, `websockets/`, `auth/`, `ssl/`.\n","frontmatter":{"paths":["tests/**/*","playwright/**/*","playwright.config.ts"]},"isInternal":false,"tokens":1561,"sizeBytes":6549},{"name":"_contract.md","path":".claude/skills/code-review/reviewers/_contract.md","rawUrl":"https://raw.githubusercontent.com/usebruno/bruno/HEAD/.claude/skills/code-review/reviewers/_contract.md","title":"Reviewers Skill","category":"anthropic-skill","format":"markdown","content":"# Shared reviewer persona & output contract\n\nRead by every `code-review` reviewer. (Orchestration lives in `../SKILL.md`; the per-lens\nchecklists live in the sibling `*.md` files.)\n\n**Persona** — an expert reviewer in TypeScript, JavaScript, Node.js, and Electron on an\nenterprise team. Be **concise**: one clear sentence per finding; elaborate only when asked.\nReview to the project's standard regardless of who authored or requested the change — never\nsoften severity for assumed intent or seniority. Ground every finding in the actual code: trust\nthe repo over any doc, guide, or comment when they disagree, and never cite a line or invent an\nexample value you haven't verified in source.\n\n**Output contract** — return a flat list, one finding per line:\n\n```\n<blocker|suggestion|nit> | <file>:<line> | <one-sentence finding>\n```\n\nReturn `no findings` (nothing else) when the scope is clean. Never invent nits to fill the list.\n","isInternal":false,"tokens":220,"sizeBytes":936},{"name":"architecture.md","path":".claude/skills/code-review/reviewers/architecture.md","rawUrl":"https://raw.githubusercontent.com/usebruno/bruno/HEAD/.claude/skills/code-review/reviewers/architecture.md","title":"Reviewers Skill","category":"anthropic-skill","format":"markdown","content":"# Architecture & dependency-boundaries reviewer\n\n**Scope:** `packages/**`.\n\nAdopt the reviewer persona and return findings in the output contract defined in\n`_contract.md`.\n\nReview the diff against **`.claude/rules/architecture.md`** — read its \"Dependency direction &\nownership boundaries\" section, plus \"Declared dependencies must match real imports\" when the diff\ntouches a `package.json`. The `@usebruno/*` packages form a strict dependency DAG and some carry hard\nplatform constraints. Report violations with `file:line`, severity:\n\n- **blocker** — a new internal `@usebruno/*` dependency that points *upward* or forms a cycle (a\n  shared lib importing `@usebruno/app` or `@usebruno/electron`); `bruno-common` importing a Node\n  built-in (`fs`/`path`/`os`/`crypto`/`child_process`/`node:*`) or any other `@usebruno/*` package\n  (it must stay browser-safe and dependency-free); `bruno-js` importing `electron`/IPC (it also runs\n  in the CLI); a runtime import of `bruno-schema-types` (types-only).\n- **suggestion** — code placed in the wrong package/layer: renderer-only logic in a shared lib,\n  host/Electron wiring pushed into `bruno-js`, or a util added to `bruno-common` that needs another\n  package; a new shared dependency that would be better kept local.\n- **suggestion** — manifest drift breaking the rule's **Declared dependencies must match real\n  imports**, read against the importing package's own `package.json`.\n- **Not a finding:** dependency edges already in the DAG, or a new *downward* dependency that fits\n  the established direction.\n","isInternal":false,"tokens":383,"sizeBytes":1566},{"name":"conventions.md","path":".claude/skills/code-review/reviewers/conventions.md","rawUrl":"https://raw.githubusercontent.com/usebruno/bruno/HEAD/.claude/skills/code-review/reviewers/conventions.md","title":"Reviewers Skill","category":"anthropic-skill","format":"markdown","content":"# Coding standards & readability reviewer\n\n**Scope:** all changed files (`**/*`).\n\nAdopt the reviewer persona and return findings in the output contract defined in\n`_contract.md`.\n\nReview the diff against **`.claude/rules/conventions.md`** (read it; it points to\n`CODING_STANDARDS.md`, the code-guidelines source of truth). Report each violation with\n`file:line`, severity:\n\n- **suggestion** — readability problems from the rule: unclear or misleading names, unnecessary\n  abstraction (indirection that earns no readability or reuse), single-line indirection, `?.` where\n  the null case isn't handled right there, needless whitespace/diff churn, or missing comments on\n  genuinely complex flow.\n- **suggestion** — any breach of the rule's **Reuse before you write** or **Replacing code leaves\n  nothing behind** sections; confirm what the new code actually renders or reads before calling a\n  leftover dead. From **Before you call it done**, report only what the diff itself shows: a new\n  branch or acceptance criterion with no test, or a changed return/payload shape whose assertions\n  elsewhere weren't updated. Whether the author ran the suite isn't visible here — don't guess at it.\n- **nit** — pure style/formatting/naming deviations (indent, quotes, semicolons, trailing commas,\n  arrow parens, casing) — most are auto-fixed by ESLint, so keep these brief.\n","isInternal":false,"tokens":308,"sizeBytes":1374},{"name":"correctness.md","path":".claude/skills/code-review/reviewers/correctness.md","rawUrl":"https://raw.githubusercontent.com/usebruno/bruno/HEAD/.claude/skills/code-review/reviewers/correctness.md","title":"Reviewers Skill","category":"anthropic-skill","format":"markdown","content":"# Correctness & root-cause reviewer\n\n**Scope:** all changed source (`**/*`, excluding `tests/**`).\n\nAdopt the reviewer persona and return findings in the output contract defined in\n`_contract.md`.\n\nValidate that the change is correct and, for every bug fix, that it addresses the\nunderlying problem rather than the visible symptom. Surface-level patches that mask a\ndefect without resolving it are a **blocker**:\n\n- Understand *why* the issue exists before accepting the fix — trace the bug to its origin,\n  don't stop at where it surfaced.\n- Flag patches that suppress a symptom (extra null guards, try/catch swallowing, defensive\n  re-checks, retries, timeouts, clamping values) while leaving the real cause in place. Ask:\n  if this defends against a bad value, *where does the bad value come from*, and should it be\n  fixed there instead?\n- A fix at the wrong layer (UI guard for a data-layer bug, caller working around a callee's\n  contract violation) is a symptom patch — call out the correct layer.\n- Watch for fixes scoped to one reproduction case when the same root cause can manifest\n  elsewhere; the correct fix usually covers all call sites, not just the reported one.\n- When a fix looks like a workaround, say so and name the deeper change that would actually\n  resolve it — even if the larger fix is out of scope, the PR should acknowledge it.\n- Beyond bug fixes, check ordinary correctness: off-by-one and boundary errors, unhandled\n  promise rejections / missing `await`, swallowed errors, incorrect null/undefined handling,\n  and edge cases the change introduces.\n- **Check the twin path.** Bruno keeps parallel implementations of the same behavior — `.bru`\n  vs `.yml` serializers, the default app-data workspace vs custom-filesystem workspaces, the\n  Electron request pipeline vs its `bruno-cli` copy, the scripting API vs its QuickJS shim,\n  `bruno-filestore` vs the `bruno-converters` export. A change that touches one path must be\n  verified against its twin; behavior that diverges between them (a value that persists or\n  defaults in one but is dropped or defaulted differently in the other) is a bug, not two\n  independent features. The full map of which surfaces pair up is\n  `.claude/rules/feature-parity.md`; the `feature-parity` lens reviews it in depth, so raise\n  only the correctness consequence here and leave the enumeration to that lens.\n- **`x || default` on a field whose absence is meaningful.** When \"not set\" / \"never\n  configured\" is a distinct state, falsy-coalescing (`version || '1'`) fabricates a value for\n  the unset case, erases the distinction, and often diverges from a sibling path that handles\n  it correctly. Flag it; use `??` or an explicit `undefined` check when the unset state must\n  survive.\n","isInternal":false,"tokens":632,"sizeBytes":2757},{"name":"cross-platform.md","path":".claude/skills/code-review/reviewers/cross-platform.md","rawUrl":"https://raw.githubusercontent.com/usebruno/bruno/HEAD/.claude/skills/code-review/reviewers/cross-platform.md","title":"Reviewers Skill","category":"anthropic-skill","format":"markdown","content":"# Cross-platform reviewer\n\n**Scope:** all changed files (`**/*`).\n\nAdopt the reviewer persona and return findings in the output contract defined in\n`_contract.md`.\n\nReview the diff against **`.claude/rules/cross-platform.md`** (read it) — Bruno ships on macOS,\nWindows, and Linux. Report each violation with `file:line`, severity:\n\n- **blocker** — anything that breaks a platform: hardcoded `/` or `\\` separators instead of\n  `path.join`/`path.resolve`; hardcoded paths (`/home/`, `C:\\Users\\`, `~/`) instead of\n  `os.homedir()`/`app.getPath()`; platform-specific shell/`child_process` without a fallback\n  (`which` vs `where`, `spawn` needing `shell:true` on Windows); Unix-only signals; `/tmp`\n  instead of `os.tmpdir()`.\n- **suggestion** — portable-but-fragile patterns not yet a concrete break: case-sensitivity\n  assumptions, inconsistent CRLF/LF handling, `fs.chmod`/`fs.access` assuming Unix permission bits.\n- **suggestion** — multiline `.bru`/text-block parsing that splits on `\\n` instead of a\n  CRLF-aware regex (`/\\r\\n|\\r|\\n/`) — see the rule's Line Endings section.\n","isInternal":false,"tokens":284,"sizeBytes":1088},{"name":"dsl-changes.md","path":".claude/skills/code-review/reviewers/dsl-changes.md","rawUrl":"https://raw.githubusercontent.com/usebruno/bruno/HEAD/.claude/skills/code-review/reviewers/dsl-changes.md","title":"Reviewers Skill","category":"anthropic-skill","format":"markdown","content":"# On-disk DSL & serialization reviewer\n\n**Scope:** `packages/bruno-app/**`, `packages/bruno-electron/**`, `packages/bruno-cli/**`,\n`packages/bruno-lang/**`, `packages/bruno-filestore/**`, `packages/bruno-schema/**`,\n`packages/bruno-schema-types/**`, `packages/bruno-converters/**`.\nThe format packages own serialization, but DSL objects are assembled in `bruno-app` (Redux)\nand `bruno-electron` (IPC), and `bruno-cli` reads/writes these files directly — a shape change\nmade there won't be caught by the serialization layer, so they're all in scope.\n\nAdopt the reviewer persona and return findings in the output contract defined in\n`_contract.md`.\n\nReview the diff against **`.claude/rules/dsl-changes.md`** (read it) — Bruno persists\ncollections/requests/environments/config as `.bru` and `.yml` files read across app versions, so\nbackward compatibility is the priority. Report violations with `file:line`, severity:\n\n- **blocker** — a breaking field change (rename, remove, retype, repurpose, or changed\n  default/semantics); a new field that isn't optional with a safe default; format drift between\n  `bru` and `yml` (or between parse and stringify); a lossy round-trip (`stringify` drops\n  unknown fields, or `parse(stringify(x)) !== x`); a shape change with no read-time compat shim;\n  a cryptic, abbreviated, or inconsistent property name (names are permanent and user-facing).\n- **blocker** — new `.bru` *syntax* (escape form, block delimiter, annotation grammar) that older\n  parsers can't read; an escaping/delimiter change not proven by on-disk reparse\n  (`parse(read(serialize(x)))`) or that escapes a multi-char delimiter by splitting on the whole\n  delimiter. See the rule's escaping and forward-compat sections.\n- **suggestion** — missing round-trip test in both formats or missing old-format golden fixture;\n  a name/structure that won't scale (positional vs keyed, widening union).\n- Also push back on any **unnecessary** DSL change that could be solved in memory / UI without\n  altering the serialized shape.\n- **Not a finding:** the read-accepts-`{content}` / write-emits-`string` asymmetry on\n  `description` fields — string-only internally is an established convention (see rule), not a\n  lossy round-trip.\n","isInternal":false,"tokens":552,"sizeBytes":2238},{"name":"e2e-tests.md","path":".claude/skills/code-review/reviewers/e2e-tests.md","rawUrl":"https://raw.githubusercontent.com/usebruno/bruno/HEAD/.claude/skills/code-review/reviewers/e2e-tests.md","title":"Reviewers Skill","category":"anthropic-skill","format":"markdown","content":"# E2E tests reviewer\n\n**Scope:** files under `tests/**` (`path: 'tests/**/**.*'`).\n\nAdopt the reviewer persona and return findings in the output contract defined in\n`_contract.md`.\n\nReview the diff against **`.claude/rules/testing.md`** and the e2e checklist in\n**`.coderabbit.yaml`** (read both). Report violations with `file:line`, severity:\n\n- **blocker** — `test.only`; `page.pause()`.\n- **suggestion** — `page.waitForTimeout()` where an `expect()` locator assertion could wait\n  instead; inline raw selectors instead of `tests/utils/page/*` modules; a spec that mutates a\n  committed fixture without restoring it in `afterAll`; shared state / non-isolated tmp paths.\n- **suggestion** — a non-discriminating assertion: `toContain('description:')` when the fixture\n  already carries that substring on unrelated rows passes even if the change under test never\n  happened; assert the unique value actually written by this change.\n- **suggestion** — parallel `.bru` and `.yml` fixtures for the same scenario that disagree\n  field-for-field (e.g. a row `selected: false` in one, enabled in the other) — a fixture bug\n  even when the current assertion tolerates it.\n- **nit** — locators not stored in variables; a single broad assertion where several focused\n  ones fit; steps not wrapped in `test.step`.\n","isInternal":false,"tokens":314,"sizeBytes":1315},{"name":"feature-parity.md","path":".claude/skills/code-review/reviewers/feature-parity.md","rawUrl":"https://raw.githubusercontent.com/usebruno/bruno/HEAD/.claude/skills/code-review/reviewers/feature-parity.md","title":"Reviewers Skill","category":"anthropic-skill","format":"markdown","content":"# Co-related features & parity reviewer\n\n**Scope:** `packages/**`.\n\nAdopt the reviewer persona and return findings in the output contract defined in\n`_contract.md`.\n\nReview the diff against **`.claude/rules/feature-parity.md`** (read it). This lens asks one\nquestion of every changed file: **what else had to change with it, and did it?** For each changed\nfile, find it in the rule's tables, open the counterpart, and check whether the counterpart logic\nis in the diff. Report with `file:line`, severity:\n\n- **blocker** — logic changed on one side of an app ↔ CLI pair (§1) when the twin file holds the\n  counterpart logic and the diff leaves it untouched. Includes bug fixes in `bruno-electron` whose\n  root cause `bruno-cli` also has a copy of. Name the twin path and function.\n- **blocker** — a scripting API added or changed in `bruno-js/src/{bru,bruno-request,bruno-response}.js`\n  without the matching bridge in `bruno-js/src/sandbox/quickjs/shims/`.\n- **blocker** — a persisted field that skips a §2 layer: missing from `bruno-schema` (throws on\n  save), the app save transform, one on-disk format, the BRU → YML migration, or OpenCollection\n  import/export (either direction). Flag the missing surface only — the on-disk contract itself\n  (compat, escaping, round-trip) belongs to the `dsl-changes` lens; don't duplicate its findings.\n- **suggestion** — identical logic patched in both runtimes that has no Electron- or CLI-only\n  dependency and could move into `@usebruno/requests` / `@usebruno/common` (`utils/cookies.js` is\n  the precedent); otherwise, a missing comment naming the pairing at both sites.\n- **suggestion** — a new app runner option with no `bru run` flag; a feature at one scope level\n  with no decision for the others or `inherit`; a wire-request change without a\n  `snippet-generator.js` update; an HTTP-only change on a path gRPC/WebSocket also flow through;\n  a Postman/Insomnia/OpenAPI converter not updated for a field its format can represent.\n- **suggestion** — tests on one side of a pair only.\n\n**Not a finding:** a stated asymmetry, or one the rule lists as known-divergent (no preferences\nstore / Electron window / system-browser OAuth / Yup validation in the CLI; CLI runner handles\nonly `http-request` and `graphql-request`). A change to runtime-specific code in a paired file\n(e.g. an Electron-only helper in `utils/filesystem.js`) is not a gap. Never claim a missing twin\nwithout confirming the twin file exists and holds the counterpart logic.\n","isInternal":false,"tokens":611,"sizeBytes":2508},{"name":"react.md","path":".claude/skills/code-review/reviewers/react.md","rawUrl":"https://raw.githubusercontent.com/usebruno/bruno/HEAD/.claude/skills/code-review/reviewers/react.md","title":"Reviewers Skill","category":"anthropic-skill","format":"markdown","content":"# React — app files reviewer\n\n**Scope:** `packages/bruno-app/**`.\n\nAdopt the reviewer persona and return findings in the output contract defined in\n`_contract.md`.\n\nReview changed components against **`CODING_STANDARDS.md` §React** (read it); for changes that\ntouch the store, also consult **`.claude/rules/redux-store.md`**. Report violations with\n`file:line`, severity:\n\n- **blocker** — an `useEffect` that could be derived state, an event handler, or a custom hook;\n  a hardcoded hex/rgb/hsl/named color instead of the styled-components `theme` prop (breaks the other\n  12 themes — verify the token path exists on the theme object); namespaced hook import (`React.useX`);\n  a component that mixes controlled and uncontrolled state; a hook called after a conditional\n  early return.\n- **blocker** — anything breaking **`.claude/rules/bruno-app-layout.md`** (read it),\n  component-specific vs shared placement included; the drift noted at the end of that file is not a\n  finding.\n- **blocker** — a new theme token defined in `themes/light|dark/*.js` without the matching property\n  added to `themes/schema/oss.js`, or added to the schema but missing from some of the 13 theme files.\n  The schema is `additionalProperties: false` and `providers/Theme/index.js` validates against it at\n  runtime, so either gap invalidates that whole theme and silently falls the user back to the default\n  with an error toast.\n- **suggestion** — a missing memo that breaks a dependency array or re-renders a heavy child, or\n  a gratuitous memo wrapping a cheap primitive; Tailwind used for colors (layout only);\n  a testable element without `data-testid`; a theme-token swap resolving to a different value than\n  the literal it replaced, or one value left hardcoded while its neighbours were themed (resolve it\n  in `themes/` first); a `document`/`window` listener not gated on the state that makes it relevant,\n  even where an incidental detail makes it harmless today.\n- **suggestion** — a new `EditableTable` column that omits the `readOnly`/`editMode` gating its\n  sibling columns carry (`readOnly={column.readOnly}` at render): the field stays editable in\n  view / read-only mode even though the change handler discards the edit.\n- **suggestion** — optimistic success state (`copied`, `saved`, `done`) set unconditionally\n  rather than gated on the operation resolving — e.g. `copied = true` after an optional-chained\n  `navigator.clipboard?.writeText` that no-ops in an insecure context — showing a false\n  confirmation.\n","isInternal":false,"tokens":598,"sizeBytes":2531},{"name":"security.md","path":".claude/skills/code-review/reviewers/security.md","rawUrl":"https://raw.githubusercontent.com/usebruno/bruno/HEAD/.claude/skills/code-review/reviewers/security.md","title":"Reviewers Skill","category":"anthropic-skill","format":"markdown","content":"# Security & data safety reviewer\n\n**Scope:** all changed source (`**/*`, excluding `tests/**`).\n\nAdopt the reviewer persona and return findings in the output contract defined in\n`_contract.md`.\n\nBruno is an offline API client that handles user credentials, tokens, and runs\nuser-authored scripts in a sandbox. Review changes for these Bruno-specific risks:\n\n- **No secret leakage.** Auth tokens, passwords, API keys, OAuth2 secrets, and env-var\n  values must never be written to logs, console, error messages, or telemetry. A secret\n  logged in cleartext is a **blocker**. Watch for `console.log`/logger calls that dump a\n  whole request, header set, or config object containing credentials.\n- **Script sandbox integrity** (`bruno-js`, QuickJS / Node VM). Changes that widen the\n  sandbox surface — exposing new Node built-ins, `require`, filesystem, or `process` to\n  user scripts, or passing unsanitized script output back into privileged code — are a\n  potential sandbox escape; call them out.\n- **IPC input validation** (`bruno-electron` handlers, `preload.js`). Treat every argument\n  arriving from the renderer as untrusted: validate file paths (guard against traversal\n  outside the collection dir), types, and bounds before acting on them.\n- **Path traversal & arbitrary writes.** File reads/writes derived from user or request\n  data must be confined to the intended directory — no `../` escape, no writing outside the\n  collection root.\n- **Injection & unsafe eval.** Flag string-built shell commands, `eval`, or dynamic\n  `Function` on untrusted input, and unescaped interpolation into commands or queries.\n- **Dependency & network surface.** Note new runtime dependencies or outbound network calls\n  that don't fit Bruno's offline-first, privacy-first posture.\n\nKeep findings concrete — tie each to how the tainted value reaches the sink.\n","isInternal":false,"tokens":410,"sizeBytes":1860},{"name":"SKILL.md","path":".claude/skills/code-review/SKILL.md","rawUrl":"https://raw.githubusercontent.com/usebruno/bruno/HEAD/.claude/skills/code-review/SKILL.md","title":"code-review","category":"anthropic-skill","format":"markdown","content":"---\nname: code-review\ndescription: Review a Bruno diff, PR, or branch via focused reviewers run in parallel —\n  correctness, security, DSL, React, cross-platform, tests. Mirrors the CodeRabbit /\n  CODING_STANDARDS review.\n---\n\n# Reviewing Bruno code\n\nThis skill mirrors the automated CodeRabbit review (`.coderabbit.yaml`) so you can run\nthe same review locally. Source-of-truth order: coding standards → `CODING_STANDARDS.md`;\narchitecture/behavior → `.claude/rules/*`. `.coderabbit.yaml` mirrors these for CI parity —\nread it for a path instruction not summarized here, but the rules win if they ever disagree.\n\nThe review is **split into focused lenses that run in parallel**, so a large diff is\ncovered faster and each reviewer stays sharply scoped to one concern. Each lens lives in\nits own file under `reviewers/`. You (the orchestrator) dispatch the reviewers, then\nmerge and report their findings.\n\n## How to review (orchestration)\n\n1. **Get the diff.** Review only what changed — never untouched code. Pick the mode:\n   - **Committed range** (default): `git diff main...HEAD` against the base branch\n     (`main` or `release/*`). Update the base first (`git fetch` and confirm the local\n     base is current) — a stale base inflates the diff with already-merged, unrelated\n     changes and wastes a full fan-out. Each reviewer re-runs this scoped to its own\n     globs; since the range is pinned to fixed commits they all see identical bytes, so\n     there's no need to snapshot.\n   - **Working tree / uncommitted changes** (when asked to review unstaged or staged\n     work): the tree can shift mid-review, so re-running `git diff` per reviewer risks\n     each seeing a different snapshot. Instead, capture the diff **once** to a scratch\n     file and hand every reviewer that path. `git diff HEAD` covers staged + unstaged\n     tracked changes; run `git add -N .` first (reversible with `git reset`) so any new\n     untracked files also show up:\n     ```bash\n     git add -N . && git diff HEAD > \"$SCRATCH/review.diff\"\n     ```\n     `$SCRATCH` is your environment's scratchpad dir. Reviewers read this frozen diff for\n     their globs plus the on-disk files for surrounding context — the working-tree files\n     already hold the uncommitted state.\n2. **Enumerate changed files** — `git diff --name-only main...HEAD` (committed range) or\n   `git diff --name-only HEAD` (working tree). Use this to skip any lens whose file scope\n   isn't touched (e.g. no `packages/bruno-app/**` change → skip `react.md`; no `tests/**`\n   change → skip `e2e-tests.md`). Never skip the lenses scoped to all files, and never skip\n   `feature-parity.md` for a small diff — a one-file change is exactly the shape it catches.\n3. **Fan out the reviewers in parallel.** In a *single message*, launch one `Agent`\n   (subagent_type `Explore` or `general-purpose`) per in-scope reviewer below. Give each\n   subagent this exact briefing:\n   - The diff source — the committed range (e.g. `main...HEAD`) or the snapshot file path\n     (`$SCRATCH/review.diff`) — and the file globs it owns (from the reviewer file's\n     \"Scope\" line). For a snapshot, tell the reviewer to read that file for its globs\n     rather than re-run `git diff`.\n   - \"Read `.claude/skills/code-review/reviewers/_contract.md` for the shared persona and\n     output contract, then read `.claude/skills/code-review/reviewers/<file>` — and any rule\n     or source file it points to (e.g. `CODING_STANDARDS.md`, `.claude/rules/*.md`), which\n     hold the detailed checklist. Apply **only** that lens to the changed files in your\n     scope, at the severities the reviewer specifies. Do not review outside your scope.\"\n   Reviewers are read-only and independent — they don't coordinate, and overlap between\n   lenses is fine (you dedupe at merge time).\n4. **Merge and report.** Collect every reviewer's findings, drop exact duplicates, and\n   when two lenses flag the same `file:line` keep the higher severity. Regroup **by\n   file**, each finding tagged by severity (blocker / suggestion / nit) with `file:line`.\n   If the review request carries a problem statement or acceptance criteria (e.g. passed\n   as args), reconcile its enumerated deliverables — docs, migration notes, a test per new\n   default/branch — against the diff and flag any that are absent. If nothing's wrong, say\n   so briefly — don't manufacture nits.\n\n## Reviewers\n\nEach file is a self-contained checklist for one lens:\n\n| Reviewer file | Lens | Scope |\n|---|---|---|\n| `reviewers/correctness.md` | Correctness & root-cause | all source (excl. `tests/**`) |\n| `reviewers/architecture.md` | Architecture & dependency boundaries | `packages/**` |\n| `reviewers/conventions.md` | Coding standards & readability | all files |\n| `reviewers/react.md` | React — app files | `packages/bruno-app/**` |\n| `reviewers/cross-platform.md` | Cross-platform (macOS/Windows/Linux) | all files |\n| `reviewers/security.md` | Security & data safety | all source (excl. `tests/**`) |\n| `reviewers/dsl-changes.md` | On-disk DSL & serialization (backward compat) | `bruno-app`, `bruno-electron`, `bruno-cli`, `bruno-lang`, `bruno-filestore`, `bruno-schema(-types)`, `bruno-converters` |\n| `reviewers/feature-parity.md` | Co-related features & parity (app↔CLI, shim, converter, level, protocol twins) | `packages/**` |\n| `reviewers/e2e-tests.md` | Playwright E2E tests | `tests/**` |\n\n## Shared reviewer persona & output contract\n\nDefined once in `reviewers/_contract.md` — the small file every reviewer reads (dispatched\nreviewers read it, not this orchestration file). Keep the persona and the\n`<blocker|suggestion|nit> | <file>:<line> | <finding>` output shape there, not duplicated here,\nso a change updates a single place.\n","frontmatter":{"name":"code-review","description":"Review a Bruno diff, PR, or branch via focused reviewers run in parallel — correctness, security, DSL, React, cross-platform, tests. Mirrors the CodeRabbit / CODING_STANDARDS review."},"isInternal":false,"tokens":1439,"sizeBytes":5767},{"name":"SKILL.md","path":".claude/skills/write-e2e-test/SKILL.md","rawUrl":"https://raw.githubusercontent.com/usebruno/bruno/HEAD/.claude/skills/write-e2e-test/SKILL.md","title":"write-e2e-test","category":"anthropic-skill","format":"markdown","content":"---\nname: write-e2e-test\ndescription: Write a Playwright E2E test for Bruno. Use when adding or modifying\n  tests under tests/, creating a new test suite, or reproducing a bug as an\n  e2e spec.\n---\n\n# Writing a Bruno E2E test\n\nRead `.claude/rules/testing.md` first for the full fixture and helper reference. See\n`docs/playwright-testing-guide.md` for the extended walkthrough (codegen, running,\ndebugging, troubleshooting) — but where it disagrees with this skill or the actual repo,\ntrust the repo: specs live in `tests/` (not `e2e-tests/`), the suite is `fullyParallel`,\nand projects are `default`, `system-pac`, `auth`, and `ssl`\n(verify in `playwright.config.ts`).\n\n## Start with codegen or by hand\n\n- **Codegen** (fastest for a new flow): `npm run test:codegen <name>` launches the app,\n  records your clicks, and writes `tests/<name>.spec.ts`. Treat the output as a rough\n  draft — move it into the right area folder, swap generated selectors for `data-testid`\n  or helpers, wrap steps in `test.step`, and delete dead recorded actions.\n- **By hand**: `import { test, expect } from '../../playwright'` and follow the steps below.\n\n## Steps\n\n1. **Place the spec** at `tests/<area>/<name>.spec.ts` — reuse an existing area dir under\n   `tests/` (list it for the current set; e.g. `request/`, `collection/`, `environments/`,\n   `runner/`, `scripting/`) or add one. Pick the project: `default` for most specs;\n   `auth` / `ssl` / `system-pac` only when the test needs those\n   servers or external dependencies.\n2. **Use fixtures** from `playwright/index.ts` (read it for the full set) instead of launching\n   the app yourself:\n   - `page` / `electronApp` — default app with onboarding skipped.\n   - `pageWithUserData` — seeds data from `tests/<area>/init-user-data/`\n     (use `init-user-data-fresh/` for new-user flows).\n   - `createTmpDir(tag?)` — throwaway dir for collections/files the test creates; cleaned\n     up automatically, so prefer it over hardcoded paths.\n   - `newPage`, `launchElectronApp`, `reuseOrLaunchElectronApp`, `restartApp` — for\n     multi-window or app-restart scenarios.\n   - `collectionFixturePath` / `workspaceFixturePath` — copy a committed fixture to a tmp\n     dir and get its path.\n3. **Use helpers** from `tests/utils/page/` instead of hand-rolling clicks — read the module\n   for the current set (e.g. `createCollection`, `sendRequest`, `runCollection`).\n4. **Structure each test** arrange → act → assert, and wrap the phases in `test.step` so\n   the HTML report reads clearly. Prefer several focused assertions over one broad check.\n5. **Wait for readiness**: `await page.locator('[data-app-state=\"loaded\"]').waitFor()`\n   before asserting on app state.\n6. **Locate** by `data-testid` (Bruno's dominant convention) or documented classes\n   (`.collection-item-name`); stable `getByRole` / `getByText` locators are fine too.\n   Avoid brittle CSS, `#id`, or index-based selectors. If no stable selector exists, add a\n   `data-testid` to the component rather than locating by text.\n7. **If the test mutates a committed fixture**, restore it in `afterAll`. Existing specs\n   do this with `git checkout <fixturePath>` (e.g. `tests/response-examples/*.spec.ts`) —\n   follow the pattern already used by neighbouring specs in that area.\n8. **Use auto-retrying assertions** (`expect(locator).toBeVisible()`); never assert\n   immediately after a file-watcher-triggered change (created files take a tick to appear).\n   Reserve `page.waitForTimeout()` for when no locator assertion can wait instead.\n9. **Run it**: `npx playwright test tests/<area>/<name>.spec.ts --project=default`\n   (the web + test servers start automatically). Debug with `--ui`, `--headed`, `--debug`,\n   or `--trace on` (then `npx playwright show-trace test-results/**/trace.zip`).\n\n## Checklist before done\n\n- [ ] Test is isolated — unique tmp paths (`createTmpDir`), no state shared with other tests\n- [ ] Passes on a clean second run (retry-safe; worker fixtures persist across retries)\n- [ ] Folders expanded before asserting on their children (collapsed = not in DOM)\n- [ ] Any mutated committed fixture is restored in `afterAll`\n- [ ] Any codegen-generated draft refactored to fixtures/helpers + stable selectors\n- [ ] Steps wrapped in `test.step`; no `test.only`, no `page.pause()`\n","frontmatter":{"name":"write-e2e-test","description":"Write a Playwright E2E test for Bruno. Use when adding or modifying tests under tests/, creating a new test suite, or reproducing a bug as an e2e spec."},"isInternal":false,"tokens":1088,"sizeBytes":4312},{"name":"README.md","path":"packages/bruno-app/storybook/README.md","rawUrl":"https://raw.githubusercontent.com/usebruno/bruno/HEAD/packages/bruno-app/storybook/README.md","title":"storybook Documentation","category":"plugin-manifest","format":"markdown","content":"# Storybook\n\nStorybook is our workbench and living documentation for the `bruno-app` UI — primarily the design-system primitives under `src/ui`. Use it to build a component in isolation, see every state at once, and check how it looks across Bruno's themes.\n\n## Running\n\nFrom the repo root:\n\n```bash\nnpm run storybook --workspace=packages/bruno-app        # dev server on http://localhost:6006\nnpm run build-storybook --workspace=packages/bruno-app  # static build -> storybook-static/\n```\n\nConfig lives in this folder: `main.js` (framework, addons, stories glob, webpack aliases) and `preview.jsx` (global decorators, the theme toolbar).\n\n## Where stories live\n\nStories are co-located next to the component they document and picked up by the glob in `main.js`:\n\n```text\nsrc/**/*.stories.@(js|jsx|mjs|ts|tsx)\n```\n\nSo `src/ui/Button/index.js` is documented by `src/ui/Button/Button.stories.jsx`. `main.js` mirrors the app's path aliases (`ui`, `components`, `themes`, `providers`, `utils`, …), so imports like `import Button from 'ui/Button'` work inside stories.\n\n## Writing a story\n\nStandard CSF. Give the component a `Components/<Name>` title and tag it `autodocs` so it gets a Docs page:\n\n```jsx\nimport Button from './index';\n\nexport default {\n  title: 'Components/Button',\n  component: Button,\n  parameters: { layout: 'centered' },\n  tags: ['autodocs'],\n  argTypes: {\n    variant: { control: 'select', options: ['filled', 'outline', 'ghost'] }\n  }\n};\n\nexport const Default = { args: { children: 'Button' } };\n```\n\nTheming is automatic — `preview.jsx` wraps every story in the styled-components `ThemeProvider` (and the app's `ThemeContext`), so components read `props.theme` and `useTheme()` normally. Don't add your own theme provider.\n\n## Theme switching\n\nThe toolbar has a **Theme** picker listing all of Bruno's registered themes (from `themes/index.js` `themeRegistry`). Selecting one re-renders the current story with that theme's real tokens and sets the preview's light/dark mode.\n\nNote: components styled via styled-components `props.theme` (nearly all of `src/ui`) get full per-theme accuracy. Anything relying on raw `var(--color-*)` CSS variables only toggles light vs dark, since those variables aren't per-theme.\n\n## Conventions & gotchas\n\nThese are worth following so components render correctly on the **Docs** page, where every story renders inline in one shared document (unlike Canvas, which isolates each story in an iframe).\n\n**Keep the Docs description clean.** Storybook uses a component's leading JSDoc as the Docs description and does not strip `@param` tags, so a rich JSDoc block dumps as a run-on paragraph. Set an explicit description in the story instead of trimming the component's source comments:\n\n```js\nparameters: {\n  docs: { description: { component: 'One clean paragraph. See the props table below.' } }\n}\n```\n\n**Don't hardcode `name` on radio-based groups.** `RadioGroup`/`SegmentedControl` auto-generate a unique `name` per instance. On the Docs page the primary story renders twice and all stories share the DOM, so a hardcoded `name` makes separate instances collide into one native radio group (selecting in one unchecks another). Omit `name` unless you specifically want a shared native group.\n\n**Give overlay components room.** Dropdowns, menus, and modals open a popover that gets clipped by the short, `overflow`-hidden inline Docs preview. Render those stories in an iframe with a height:\n\n```js\nparameters: { docs: { story: { inline: false, height: '360px' } } }\n```\n\n**Always give group components an accessible name.** Components like `RadioGroup`/`SegmentedControl` warn in development if they render without `label`, `ariaLabel`, or `ariaLabelledBy` — pass one in stories and real usage. Don't fabricate a placeholder name to silence the warning; a meaningless label is worse than none (only the shared native `name` attribute is auto-generated, because it carries no human meaning).\n\n**Inline story state — don't wrap in a helper component.** For controlled components, put `useState` directly inside the story's `render` and return the component itself. Wrapping it in a `<Controlled>` helper makes the code panel show `<Controlled>` instead of the real component, because the generated source reflects the root element the `render` returns.\n\n```jsx\nexport const Default = {\n  render: (args) => {\n    const [value, setValue] = useState('http');\n    return <SegmentedControl {...args} value={value} onChange={setValue} items={items} />;\n  }\n};\n```\n\n**Demo wrappers, not component chrome.** If a story needs a sized/bordered container to demonstrate behavior (e.g. width for overflow), keep it clearly a demo wrapper so it isn't mistaken for the component's own styling.\n\n## Docs addon\n\nAutodocs (the Docs tab) requires `@storybook/addon-docs`, registered in `main.js` and pinned to the **same version** as the `storybook` core package — a version mismatch throws `docsParameter.renderer is not a function`. Run `npx storybook doctor` to check for mismatched Storybook packages.\n\n## Code panel\n\n`preview.jsx` turns on the source/code views that `@storybook/addon-docs` provides:\n\n```js\ndocs: {\n  codePanel: true,              // a \"Code\" tab in the Canvas addon panel\n  canvas: { sourceState: 'shown' } // Docs \"Show code\" expanded by default\n}\n```\n\nThe snippet reflects the root element your story's `render` returns, so follow the \"inline story state\" note above to keep it showing the real component. There's also a `</>` Show code button in the Canvas toolbar.\n\n## Adding a new primitive\n\n1. Build the component under `src/ui/<Name>/` (`index.js` + `StyledWrapper.js`), following the existing conventions: `$`-prefixed transient style props, theme tokens, a `dataTestId`, forwarded refs, and forwarded `...rest`.\n2. Add `<Name>.stories.jsx` next to it with a `Components/<Name>` title, `autodocs`, `argTypes`, a clean `docs.description.component`, and a story per meaningful state.\n3. Add `<Name>.spec.jsx` (React Testing Library) for behavior and accessibility.\n\n### Group primitives (single-select)\n\nSingle-select group primitives (`RadioGroup`, `SegmentedControl`) follow a specific layout: the implementation lives in a `components/` subfolder and only the data-driven **list** API is exposed publicly, keeping the compound pieces internal so a compound API can be surfaced later without a rewrite.\n\n```text\nsrc/ui/<Name>/\n├── components/\n│   ├── <Name>Base.jsx    # compound: owns the context + renders the radiogroup\n│   ├── <Option>.jsx      # the internal option (reads context)\n│   ├── <Name>.jsx        # public list component — maps `items` → options\n│   └── StyledWrapper.js  # styling\n├── index.jsx             # exposes only the list <Name> (default export)\n├── <Name>.stories.jsx\n└── <Name>.spec.jsx\n```\n\nConsumers use the list API: `<SegmentedControl value onChange items={[{ value, label, ... }]} />`. To expose the compound API later, re-export `<Option>` / `<Name>Base` from `index.jsx`.\n","isInternal":false,"tokens":1665,"sizeBytes":7039},{"name":"README.md","path":"packages/bruno-cli/docker/images/alpine/README.md","rawUrl":"https://raw.githubusercontent.com/usebruno/bruno/HEAD/packages/bruno-cli/docker/images/alpine/README.md","title":"alpine Documentation","category":"plugin-manifest","format":"markdown","content":"# Bruno CLI — Alpine\n\nAlpine Linux variant of the Bruno CLI Docker image.\n\n**Base image:** `node:22-alpine`\n\n## Building\n\n```bash\ndocker build -t usebruno/cli:alpine ./images/alpine\n\n# with specific Bruno CLI version\ndocker build \\\n  --build-arg BRUNO_VERSION=3.3.0 \\\n  -t usebruno/cli:3.3.0-alpine \\\n  ./images/alpine\n```\n\n## Usage\n\n```bash\n# Run a collection\ndocker run -v $(pwd):/bruno usebruno/cli:alpine run\n\n# with pinned version\ndocker run -v $(pwd):/bruno usebruno/cli:3.3.0-alpine run\n```\n","isInternal":false,"tokens":161,"sizeBytes":500},{"name":"README.md","path":"packages/bruno-cli/docker/images/debian/README.md","rawUrl":"https://raw.githubusercontent.com/usebruno/bruno/HEAD/packages/bruno-cli/docker/images/debian/README.md","title":"debian Documentation","category":"plugin-manifest","format":"markdown","content":"# Bruno CLI — Debian\n\nDebian slim variant of the Bruno CLI Docker image.\n\n**Base image:** `node:22-slim`\n\n## Building\n\n```bash\ndocker build -t usebruno/cli:debian ./images/debian\n\n# with specific Bruno CLI version\ndocker build \\\n  --build-arg BRUNO_VERSION=3.3.0 \\\n  -t usebruno/cli:3.3.0-debian \\\n  ./images/debian\n```\n\n## Usage\n\n```bash\n# Run a collection\ndocker run -v $(pwd):/bruno usebruno/cli:debian run \n\n# with pinned version\ndocker run -v $(pwd):/bruno usebruno/cli:3.3.0-debian run \n```\n","isInternal":false,"tokens":159,"sizeBytes":499},{"name":"README.md","path":"packages/bruno-cli/docker/README.md","rawUrl":"https://raw.githubusercontent.com/usebruno/bruno/HEAD/packages/bruno-cli/docker/README.md","title":"docker Documentation","category":"plugin-manifest","format":"markdown","content":"# Bruno CLI Docker Images\n\nOfficial Docker images for [Bruno CLI](https://www.usebruno.com), enabling container-native API collection runs in CI/CD pipelines and local environments without requiring Node.js or npm on the host. See the [Bruno CLI docs](https://docs.usebruno.com/bru-cli/overview) for CLI usage.\n\n## Registries\n\n```bash\ndocker pull usebruno/cli:latest\ndocker pull ghcr.io/usebruno/cli:latest\n```\n\n---\n\n## Variants\n\n| Variant | Base image | Details |\n|---------|-----------|---------|\n| **Alpine** (default) | `node:22-alpine` | [→ Alpine README](https://github.com/usebruno/bruno/blob/main/packages/bruno-cli/docker/images/alpine/README.md) |\n| **Debian** | `node:22-slim` | [→ Debian README](https://github.com/usebruno/bruno/blob/main/packages/bruno-cli/docker/images/debian/README.md) |\n\n### Quick choice\n\n- **Use Alpine** unless you have a specific reason not to (90% of users)\n- **Use Debian** if you hit SSL/glibc compatibility issues\n\n---\n\n## Tags\n\nEvery release publishes the following tags to **both** Docker Hub (`usebruno/cli`) and GHCR (`ghcr.io/usebruno/cli`).\n\n### Alpine variant (default)\n\n| Tag pattern | Example | Notes |\n|-------------|---------|-------|\n| `latest` | `usebruno/cli:latest` | Newest release marked as latest. Only moves when the publish workflow is run with \"Tag this version as latest\" checked. |\n| `latest-alpine` | `usebruno/cli:latest-alpine` | Alias of `latest` — also alpine. |\n| `alpine` | `usebruno/cli:alpine` | Newest alpine, moves on every alpine publish. |\n| `<version>` | `usebruno/cli:3.3.0` | Exact version, immutable. |\n| `<version>-alpine` | `usebruno/cli:3.3.0-alpine` | Exact version, explicitly alpine. |\n| `<major.minor>` | `usebruno/cli:3.3` | Floats with patch releases (3.3.x). |\n| `<major.minor>-alpine` | `usebruno/cli:3.3-alpine` | Same, explicitly alpine. |\n| `<major>` | `usebruno/cli:3` | Floats with any 3.x.x release. |\n| `<major>-alpine` | `usebruno/cli:3-alpine` | Same, explicitly alpine. |\n\n### Debian variant\n\n| Tag pattern | Example | Notes |\n|-------------|---------|-------|\n| `latest-debian` | `usebruno/cli:latest-debian` | Newest debian release marked as latest (gated by the same checkbox). |\n| `debian` | `usebruno/cli:debian` | Newest debian, moves on every debian publish. |\n| `<version>-debian` | `usebruno/cli:3.3.0-debian` | Exact version, debian. |\n| `<major.minor>-debian` | `usebruno/cli:3.3-debian` | Floats with debian patch releases. |\n| `<major>-debian` | `usebruno/cli:3-debian` | Floats with any 3.x.x debian release. |\n\nThe unsuffixed tags (`:latest`, `:3.3.0`, `:3.3`, `:3`, `:alpine`) always resolve to the alpine variant by convention.\n\n---\n\n## Step-by-step guide\n\n### Step 1 — Pull the image\n\n```bash\n# latest (alpine by default — smallest, fastest to pull)\ndocker pull usebruno/cli:latest\n\n# specific version (recommended for production CI)\ndocker pull usebruno/cli:3.3.0\n\n# major.minor — gets patch updates automatically\ndocker pull usebruno/cli:3.3\n\n# debian variant\ndocker pull usebruno/cli:debian\ndocker pull usebruno/cli:3.3.0-debian\n```\n\n---\n\n#### Check it works\n\n```bash\ndocker run --rm usebruno/cli --version\n```\n\n---\n\n### Step 2 — Run your collection\n\n> These examples assume you are running `docker` from your Bruno collection directory. Mount that directory to `/bruno` and pass `bru` arguments directly after the image name. If your collection lives elsewhere on disk, see the path-based examples further down.\n\n> **Cross-platform note:** the examples below use `$(pwd)` which works in Bash / Zsh / Git Bash / WSL. On Windows native shells, substitute `$(pwd)` with `${PWD}` (PowerShell) or `%cd%` (CMD).\n\n> **Adding `bru` options:** The examples below show docker-specific flags. For all `bru run` options — recurse (`-r`), environments, variables, reporters, bail, and more — see the [Bruno CLI docs](https://docs.usebruno.com/bru-cli/overview).\n\n```bash\n# run every request in the Bruno collection (current dir)\ndocker run -v $(pwd):/bruno usebruno/cli run \n\n# run a specific subfolder (group of requests) within that collection\ndocker run -v $(pwd):/bruno usebruno/cli run ./api-tests\n\n# run a single .bru request file from that collection\ndocker run -v $(pwd):/bruno usebruno/cli run ./api-tests/login.bru\n\n# write a JUnit XML report (lands in the current directory because of the bind mount)\ndocker run -v $(pwd):/bruno usebruno/cli run --reporter-junit results.xml\n```\n\nFor Windows CMD users, swap `$(pwd)` with `%cd%`:\n\n```cmd\ndocker run -v %cd%:/bruno usebruno/cli run \n```\n\n#### Running a collection that lives at a different path\n\nIf your collection is not in your current directory, point `docker` at its path (relative or absolute) instead of `$(pwd)`:\n\n```bash\n# run every request in a collection at an arbitrary path\ndocker run -v /path/to/your/collection:/bruno usebruno/cli run\n\n# run a single .bru file from a collection at an arbitrary path\ndocker run -v /path/to/your/collection:/bruno usebruno/cli run ./auth/login.bru\n```\n\n> **Note on `--rm`:** Examples below include `--rm`. Docker keeps stopped containers around after they exit, which lets you `docker logs` or `docker inspect` them later for debugging. If you'd rather have Docker auto-delete the container as soon as `bru` finishes — useful for CI runs or to avoid `docker ps -a` filling up with stale entries — append `--rm` to any `docker run` (or `docker compose run`) command:\n\n```bash\ndocker run --rm -v $(pwd):/bruno usebruno/cli run\n```\n\n> It's purely a cleanup convenience; it doesn't affect the image, mounts, stdout output, or exit code.\n\n\n## CI/CD integration\n\n### GitHub Actions\n\n```yaml\njobs:\n  api-tests:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n\n      - name: Run Bruno collection\n        run: |\n          docker run --rm \\\n            -v ${{ github.workspace }}:/bruno \\\n            usebruno/cli:latest run --output results.xml --format junit\n\n      - name: Publish Test Report\n        uses: dorny/test-reporter@v3\n        if: success() || failure()\n        with:\n          name: Bruno Test Results\n          path: ${{github.workspace}}/results.xml\n          reporter: java-junit\n```\n\n### GitLab CI\n\n```yaml\napi-tests:\n  image: usebruno/cli:latest\n  script:\n    - bru run --output results.xml --format junit\n  artifacts:\n    reports:\n      junit: results.xml\n```\n\n---\n\n## Docker Compose\n\n### Quick example\n\nA minimal `docker-compose.yml` for running a Bruno collection alongside your project:\n\n```yaml\nservices:\n  bruno-cli:\n    image: usebruno/cli:latest\n    container_name: bruno-cli-runner\n    volumes:\n      - /path/to/collection:/bruno\n      - /path/to/reports:/reports\n    command:\n      run .\n      -r\n      --env ci\n      --reporter-json /reports/results.json\n      --reporter-junit /reports/results.xml\n      --reporter-html /reports/results.html\n```\n\nThen run:\n\n```bash\ndocker compose run bruno-cli\n```\n\nThe `/path/to/reports:/reports` mount catches the JSON, JUnit XML, and HTML reports on the host — drop any `--reporter-*` flag to skip that format.\n\n### Try it from this repo\n\nA ready-to-run `docker-compose.yml` lives in this repo at [`packages/bruno-tests/docker-compose.yml`](https://github.com/usebruno/bruno/blob/main/packages/bruno-tests/docker-compose.yml). It mounts the sibling `collection/` directory into the container, runs the `echo` folder against the `Prod` environment, and writes JSON, JUnit XML, and HTML reports into `packages/bruno-tests/reports/`:\n\n```bash\ncd packages/bruno-tests\ndocker compose run bruno-cli\n```\n\nThis fires a small set of requests against public endpoints that demonstrate the CLI executing requests and assertions inside a container.\n\n### Standalone demo\n\nFor a clone-and-run demo with a curated collection, see [`bruno-collections/bruno-cli-docker`](https://github.com/bruno-collections/bruno-cli-docker).\n\n---\n\n## Image details\n\nAll variants include:\n\n- **Entrypoint:** `bru`\n- **Working directory:** `/bruno`\n- **User:** `node` (UID 1000, non-root)\n- **Architectures:** `linux/amd64`, `linux/arm64`\n\n","isInternal":false,"tokens":2159,"sizeBytes":8031},{"name":"readme.md","path":"packages/bruno-cli/readme.md","rawUrl":"https://raw.githubusercontent.com/usebruno/bruno/HEAD/packages/bruno-cli/readme.md","title":"bruno-cli Documentation","category":"plugin-manifest","format":"markdown","content":"# Bruno CLI\n\nWith Bruno CLI, you can now run your API collections with ease using simple command line commands.\n\nThis makes it easier to test your APIs in different environments, automate your testing process, and integrate your API tests with your continuous integration and deployment workflows.\n\nFor detailed documentation, visit [Bruno CLI Documentation](https://docs.usebruno.com/bru-cli/overview).\n\n## Installation\n\nTo install the Bruno CLI, use the node package manager of your choice, such as NPM:\n\n```bash\nnpm install -g @usebruno/cli\n```\n\n## Getting started\n\nNavigate to the directory where your API collection resides, and then run:\n\n```bash\nbru run\n```\n\nThis command will run all the requests in your collection. You can also run a single request by specifying its filename:\n\n```bash\nbru run request.bru\n```\n\nOr run all requests in a collection's subfolder:\n\n```bash\nbru run folder\n```\n\nIf you need to use an environment, you can specify it with the `--env` option:\n\n```bash\nbru run folder --env Local\n```\n\nIf you need to collect the results of your API tests, you can specify the `--output` option:\n\n```bash\nbru run folder --output results.json\n```\n\nIf you need to run a set of requests that connect to peers with both publicly and privately signed certificates respectively, you can add private CA certificates via the `--cacert` option. By default, these certificates will be used in addition to the default truststore:\n\n```bash\nbru run folder --cacert myCustomCA.pem\n```\n\nIf you need to limit the trusted CA to a specified set when validating the request peer, provide them via `--cacert` and in addition use `--ignore-truststore` to disable the default truststore:\n\n```bash\nbru run request.bru --cacert myCustomCA.pem --ignore-truststore\n```\n\n## Importing Collections\n\nYou can import collections from other formats, such as OpenAPI, using the import command:\n\n```bash\nbru import openapi --source api.yml --output ~/Desktop/my-collection --collection-name \"My API\"\n```\n\nYou can also use the shorter form with aliases:\n\n```bash\nbru import openapi -s api.yml -o ~/Desktop/my-collection -n \"My API\"\n```\n\nThis creates a Bruno collection directory that can be opened in Bruno.\n\nYou can also import directly from a URL:\n\n```bash\nbru import openapi --source https://example.com/api-spec.json --output ~/Desktop --collection-name \"Remote API\"\n```\n\nYou can also export the collection as a JSON file:\n\n```bash\nbru import openapi --source api.yml --output-file ~/Desktop/my-collection.json --collection-name \"My API\"\n```\n\nImport Options:\n\n| Option                    | Details                                            |\n| ------------------------- | -------------------------------------------------- |\n| --source, -s              | Path to the source file or URL (required)          |\n| --output, -o              | Path to the output directory                       |\n| --output-file, -f         | Path to the output JSON file                       |\n| --collection-name, -n     | Name for the imported collection                   |\n| --insecure                | Skip SSL certificate validation when fetching from URLs |\n\n## Command Line Options\n\n| Option                       | Details                                                                       |\n| ---------------------------- | ----------------------------------------------------------------------------- |\n| -h, --help                   | Show help                                                                     |\n| --version                    | Show version number                                                           |\n| -r                           | Indicates a recursive run (default: false)                                    |\n| --cacert [string]            | CA certificate to verify peer against                                         |\n| --env [string]               | Specify environment to run with                                               |\n| --env-var [string]           | Overwrite a single environment variable, multiple usages possible             |\n| -o, --output [string]        | Path to write file results to                                                 |\n| -f, --format [string]        | Format of the file results; available formats are \"json\" (default) or \"junit\" |\n| --reporter-json [string]     | Path to generate a JSON report                                                |\n| --reporter-junit [string]    | Path to generate a JUnit report                                               |\n| --reporter-html [string]     | Path to generate an HTML report                                               |\n| --insecure                   | Allow insecure server connections                                             |\n| --tests-only                 | Only run requests that have tests                                             |\n| --bail                       | Stop execution after a failure of a request, test, or assertion               |\n| --csv-file-path              | CSV file to run the collection with                                           |\n| --reporter--skip-all-headers | Skip all headers in the report                                                |\n| --reporter-skip-headers      | Skip specific headers in the report                                           |\n| --client-cert-config         | Client certificate configuration by passing a JSON file                       |\n| --delay [number]             | Add delay to each request                                                     |\n\n## Scripting\n\nBruno cli returns the following exit status codes:\n\n- `0` -- execution successful\n- `1` -- an assertion, test, or request in the executed collection failed\n- `2` -- the specified output directory does not exist\n- `3` -- the request chain seems to loop endlessly\n- `4` -- bru was called outside of a collection root directory\n- `5` -- the specified input file does not exist\n- `6` -- the specified environment does not exist\n- `7` -- the environment override was not a string or object\n- `8` -- an environment override is malformed\n- `9` -- an invalid output format was requested\n- `255` -- another error occurred\n\n## Demo\n\n![demo](assets/images/cli-demo.png)\n\n## Support\n\nIf you encounter any issues or have any feedback or suggestions, please raise them on our [GitHub repository](https://github.com/usebruno/bruno)\n\nThank you for using Bruno CLI!\n\n## Changelog\n\n<!-- An absolute link is used here because npm treats links differently -->\n\nSee [https://github.com/usebruno/bruno/releases](https://github.com/usebruno/bruno/releases)\n\n## License\n\n[MIT](license.md)\n","isInternal":false,"tokens":1280,"sizeBytes":6609},{"name":"readme.md","path":"packages/bruno-common/readme.md","rawUrl":"https://raw.githubusercontent.com/usebruno/bruno/HEAD/packages/bruno-common/readme.md","title":"bruno-common Documentation","category":"plugin-manifest","format":"markdown","content":"# bruno-common\n\nA collection of common utilities used across Bruno App, Electron and CLI packages.\n\n### Publish to Npm Registry\n\n```bash\nnpm publish --access=public\n```\n","isInternal":false,"tokens":39,"sizeBytes":169},{"name":"readme.md","path":"packages/bruno-converters/readme.md","rawUrl":"https://raw.githubusercontent.com/usebruno/bruno/HEAD/packages/bruno-converters/readme.md","title":"bruno-converters Documentation","category":"plugin-manifest","format":"markdown","content":"# bruno-converters\n\nThe converters package is responsible for converting collections from one format to a Bruno collection.\nIt can be used as a standalone package or as a part of the Bruno framework.\n\n## Installation\n\n```bash\nnpm install @usebruno/converters\n```\n\n## Usage\n\n### Convert Postman collection to Bruno collection\n\n```javascript\nconst { postmanToBruno } = require('@usebruno/converters');\n\n// Convert Postman collection to Bruno collection\nconst brunoCollection = postmanToBruno(postmanCollection);\n```\n\n### Convert Postman Environment to Bruno Environment\n\n```javascript\nconst { postmanToBrunoEnvironment } = require('@usebruno/converters');\n\nconst brunoEnvironment = postmanToBrunoEnvironment(postmanEnvironment);\n```\n\n### Convert Insomnia collection to Bruno collection\n\n```javascript\nconst { insomniaToBruno } = require('@usebruno/converters');\n\nconst brunoCollection = insomniaToBruno(insomniaCollection);\n```\n\n### Convert OpenAPI specification to Bruno collection\n\n```javascript\nconst { openApiToBruno } = require('@usebruno/converters');\n\nconst brunoCollection = openApiToBruno(openApiSpecification);\n```\n\n### Convert WSDL file to Bruno collection\n\n```javascript\nimport { wsdlToBruno } from '@usebruno/converters';\n\nconst brunoCollection = await wsdlToBruno(wsdlContent);\n```\n\n## Example \n\n```javascript\n\nconst { postmanToBruno } = require('@usebruno/converters');\nconst { writeCollectionToDisk } = require('@usebruno/filestore');\nconst fs = require('fs/promises');\n\nasync function convertPostmanToBruno(inputFile, outputDir) {\n  const inputData = await fs.readFile(inputFile, 'utf8');\n\n  const collection = await postmanToBruno(JSON.parse(inputData));\n  // Writes YAML (OpenCollection) files by default\n  await writeCollectionToDisk(collection, outputDir);\n  // Pass { format: 'bru' } to write .bru files instead\n  // await writeCollectionToDisk(collection, outputDir, { format: 'bru' });\n}\n\nconvertPostmanToBruno('demo_collection.postman_collection.json', './bruno-collection');\n\n``` \n\n## WSDL Import Features\n\nThe WSDL importer supports the following features:\n\n- **Service Discovery**: Automatically extracts service endpoints from WSDL definitions\n- **Operation Mapping**: Converts WSDL operations to Bruno HTTP requests\n- **SOAP Envelope Generation**: Creates proper SOAP envelopes for each operation\n- **Header Configuration**: Sets up appropriate Content-Type and SOAPAction headers\n- **Environment Variables**: Creates environment variables for service base URLs\n- **Folder Organization**: Groups operations by port type for better organization\n\n### WSDL Import Example\n\n```javascript\nimport { wsdlToBruno } from '@usebruno/converters';\nimport { writeCollectionToDisk } from '@usebruno/filestore';\nimport fs from 'fs/promises';\n\nasync function importWSDL() {\n  const wsdlContent = await fs.readFile('service.wsdl', 'utf8');\n\n  const collection = await wsdlToBruno(wsdlContent);\n  // Writes YAML (OpenCollection) files by default\n  await writeCollectionToDisk(collection, './soap-collection');\n  // Pass { format: 'bru' } to write .bru files instead\n  // await writeCollectionToDisk(collection, './soap-collection', { format: 'bru' });\n}\n\nimportWSDL();\n```\n\n### CLI Usage\n\nYou can also use the Bruno CLI to import WSDL files:\n\n```bash\n# Import WSDL file to a directory\nbruno import wsdl --source service.wsdl --output ~/Desktop/soap-collection --collection-name \"SOAP Service\"\n\n# Import WSDL from URL\nbruno import wsdl --source https://example.com/service.wsdl --output ~/Desktop --collection-name \"Remote SOAP Service\"\n\n# Import WSDL and save as JSON file\nbruno import wsdl --source service.wsdl --output-file ~/Desktop/soap-collection.json --collection-name \"SOAP Service\"\n```\n\n## Supported Formats\n\n- **Postman Collections** (v2.1)\n- **Insomnia Collections** (v4 and v5)\n- **OpenAPI Specifications** (v3.0)\n- **WSDL Files** (Web Services Description Language)\n\n## Dependencies\n\n- `lodash` - Utility functions\n- `nanoid` - UUID generation\n- `js-yaml` - YAML parsing\n- `xml2js` - XML parsing for WSDL\n- `@usebruno/schema` - Schema validation\n","isInternal":false,"tokens":962,"sizeBytes":4069},{"name":"README.md","path":"packages/bruno-converters/tests/postman/round-trip/README.md","rawUrl":"https://raw.githubusercontent.com/usebruno/bruno/HEAD/packages/bruno-converters/tests/postman/round-trip/README.md","title":"round-trip Documentation","category":"plugin-manifest","format":"markdown","content":"# Round-trip conversion tests\n\nThis directory tests conversion **fidelity** by sending a real collection through both converters and\ncomparing the result against the input:\n\n```\nPostman export ──postmanToBruno──▶ Bruno collection ──brunoToPostman──▶ Postman export\n     (input)                                                              (output)\n        └──────────────────── compared, subtree by subtree ──────────────────┘\n```\n\nCurrently only the **auth** subtree is covered ([`auth.spec.js`](./auth.spec.js)), but the harness is\nbuilt so other subtrees (body, headers, scripts, params, …) can be added as sibling files with no new\nplumbing. See [Extending](#extending) below.\n\n---\n\n## Why round-trip, and why this direction\n\nThe per-converter unit specs (`process-auth`, `request-auth`, `bruno-to-postman`, …) each exercise\n**one** converter in isolation and assert a hand-written expected object. That has two blind spots:\n\n1. **Asymmetry between the two converters** — a field renamed on import (`tokenName` → `credentialsId`)\n   but not renamed back on export is invisible to a one-sided test. A round-trip is the only thing that\n   exercises both mappings against each other.\n2. **Circular expectations** — a hand-written \"expected\" object is only as correct as the author's\n   reading of the converter. If you misread it, the test encodes the misreading.\n\nWe focus on **Postman → Bruno → Postman** (not Bruno → Postman → Bruno) because that is the direction\nwhere a **real exported `.postman_collection.json`** is the input. Real exports carry the exact key\nnames, the full v2.1 array-backed auth format, and even fields Bruno doesn't model — ground truth that\na hand-built object can't reproduce.\n\n---\n\n## Why the fixture approach\n\nTests are **data-driven off the files in [`fixtures/`](./fixtures)**. Every `*.postman_collection.json`\nis discovered automatically and run through the round-trip. This is deliberate:\n\n- **No hand-written expected values.** The input fixture's own auth is the source of truth — the test\n  asserts the output equals the input (minus documented exceptions), so adding coverage is \"drop a\n  file\", not \"write an expectation\".\n- **Real-world shape.** Fixtures are actual Postman exports, so they include the quirks (omitted\n  defaults, extra keys, ordering) that synthetic objects miss.\n\n### Extending\n\n- **More auth coverage:** export a collection of the desired auth type from Postman, drop it into\n  `fixtures/`. It's picked up automatically. A new whitelist entry is only needed if it exercises a\n  field Bruno doesn't model yet (see below).\n- **Another subtree** (e.g. body): add `body.spec.js` alongside `auth.spec.js`, reusing the same\n  fixtures. Factor the shared \"walk + diff by node path\" logic the way [`compare-auth.js`](./compare-auth.js)\n  does for auth, keeping each subtree's normalization rules in its own module.\n\n---\n\n## The whitelist and its categories\n\nComparison is **strict**: any difference in the auth subtree fails the test. Differences that are\nexpected today are recorded in `AUTH_ROUNDTRIP_WHITELIST` in [`auth.spec.js`](./auth.spec.js), each\ntagged with a `category`. The category is the whole point — it separates \"bug we haven't fixed\" from\n\"working as intended\":\n\n| Category | Meaning | Trend | Action |\n| --- | --- | --- | --- |\n| **`feature-gap`** | Bruno has no field to hold this Postman value, so it's dropped on import. | **Should shrink** as Bruno adds support. | **This is the list to focus on.** Each entry is a concrete, user-visible data loss. |\n| **`normalization`** | Intentional, semantically-lossless transformation. | **Permanent** — will never reach zero. | Leave alone. Documents *why* the round-trip isn't byte-identical. |\n\n**Where to focus: the `feature-gap` count.** It's the number of Postman auth capabilities Bruno can't\nrepresent. Driving it down is real product work; when a gap is filled, delete its whitelist entry and\nthe test stays green. The `normalization` entries are not a backlog — they're the documented contract\nof what round-tripping intentionally changes.\n\n### The two flavors of `normalization`\n\n- **Grant-scoped pruning** (oauth2): Bruno stores grant-type-specific fields, so a field unused by the\n  grant (`authUrl` on a `client_credentials` grant, `username` on anything but `password`) is dropped.\n  These entries carry a `grantType` list and are accepted **only** for grants that don't use the field —\n  so the *same* drop on a grant that *does* use it (e.g. `authUrl` on `authorization_code`) still fails.\n  That keeps the exception tight instead of blanket-allowing the field to vanish.\n- **Default materialization**: Postman omits a field and relies on a default; Bruno stores it\n  explicitly and re-emits it (apikey `in: header`, oauth1 `version`). Whitelisted only where **Bruno's\n  default equals Postman's default**, so materializing it changes nothing semantically.\n\n### Whitelist entry matching\n\nEach entry matches a diff on `(fixture, node, authType, key, kind)`:\n\n- `fixture` and `node` accept `'*'` — a field-level gap that recurs across many requests/fixtures is\n  **one wildcard entry**, not dozens of near-duplicates.\n- `grantType` (oauth2, optional) restricts an entry to specific grant types (grant-scoped pruning).\n- Matching is **value-agnostic** (`kind` + `key`, never the value) so a whitelisted field tolerates any\n  value — we're asserting \"this field is *expected* to differ\", not pinning a specific value.\n\n---\n\n## Comparison mechanics (`compare-auth.js`)\n\n### `stableStringify`\n\nAuth values are compared by a **canonical stringification** — object keys sorted, array order\npreserved — rather than by `JSON.stringify` or reference equality. Two reasons:\n\n1. **Key order is not semantic.** Postman and Bruno emit auth params in different orders; a plain\n   string compare would flag every reorder as a diff. Sorting keys makes the compare structural.\n2. **Nested values must compare by content.** The oauth2 `additionalParameters` arrays\n   (`authRequestParams` / `tokenRequestParams` / `refreshRequestParams`) are arrays of objects. We need\n   deep, order-insensitive-on-keys but order-*sensitive*-on-arrays comparison, which `stableStringify`\n   gives: object keys are sorted (order-insensitive), array element order is preserved (array order *is*\n   semantic for these params).\n\n### `normalizeAuth` — array-backed → map\n\nPostman's v2.1 auth is an array of `{key, value, type}`. We collapse it to a plain `{key: value}` map so\nthat (a) param ordering doesn't matter and (b) the `type` tag is ignored because it is not semantically meaningful. \nMissing auth normalizes to `null` so \"no auth\" compares equal on both sides.\n\n### `absent === \"\"` collapse\n\nAn absent key and an empty-string value are treated as **equal**. Postman treats a missing auth param as\nunset, and `brunoToPostman` drops empty params on export — so `state: \"\"` disappearing on round-trip is a\nlossless no-op, not a diff. Collapsing this structurally (rather than whitelisting each empty field)\nkeeps the whitelist focused on real differences and avoids churn when a fixture's empty fields change.\nConfirmed safe: Postman's importer defaults any missing auth param, so omitting empties never errors.\n\n**Exception — keys with a non-empty Postman default.** \"Absent == unset\" is false for a param Postman\nomits only because it sits at a non-empty default it still applies: oauth2 `headerPrefix` omitted means\n`Bearer`, so collapsing it would hide Bruno dropping the prefix. `POSTMAN_NON_EMPTY_DEFAULTS` in\n`compare-auth.js` materializes these on the *original* side (key absent or `null`), each with an\n`appliesWhen` predicate since the default is usually conditional — `headerPrefix` applies when the token\ngoes in the header, and an absent `addTokenTo` counts as header because Postman resolves an unspecified\nplacement to Request Headers. These predicates model **Postman's** defaults, never Bruno's importer:\nmatching the importer would make the oracle ratify it instead of checking it.\n\n### `grantType` on oauth2 diffs\n\nEach oauth2 diff is tagged with the node's grant type (from the original, falling back to the\nround-tripped side). This is what lets grant-scoped-pruning whitelist entries be conditional — the diff\ncarries enough context to say \"dropped `authUrl`, and this was a `client_credentials` grant, so it's\nfine.\"\n\n### Node-path walk\n\n`collectAuthNodes` walks the whole collection — collection-level auth, folder-level auth, and\nrequest-level auth — keying each by a stable path (`collection`, `collection/Folder`,\n`collection/Folder/Request`). Comparison is per-node by path, so a request that gains or loses auth, or\ninherits differently, is caught rather than averaged away.\n\n---\n\n## Running\n\n```bash\n# from packages/bruno-converters\nnpx jest tests/postman/round-trip/auth.spec.js\n```\n\nOn failure, each un-whitelisted diff prints a readable line — `DROPPED` (Bruno lost a field),\n`ADDED` (Bruno emitted a field Postman didn't), or `CHANGED` (value differs) — so you can tell at a\nglance whether it's a new feature gap to whitelist or a real regression to fix.\n","isInternal":false,"tokens":2171,"sizeBytes":9250},{"name":"readme.md","path":"packages/bruno-docs/readme.md","rawUrl":"https://raw.githubusercontent.com/usebruno/bruno/HEAD/packages/bruno-docs/readme.md","title":"bruno-docs Documentation","category":"plugin-manifest","format":"markdown","content":"# bruno-docs\n\nThis is a wip.\n\nWe have a request to generate docs in a html file that can be hosted on a server so that the visitor can view the API and make requests without downloading/installing anything.","isInternal":false,"tokens":47,"sizeBytes":206},{"name":"readme.md","path":"packages/bruno-electron/readme.md","rawUrl":"https://raw.githubusercontent.com/usebruno/bruno/HEAD/packages/bruno-electron/readme.md","title":"bruno-electron Documentation","category":"plugin-manifest","format":"markdown","content":"# bruno-electron\n\n```bash\n# electron dev\nnpm start\n\n# generate pfx file for signing windows build\nopenssl pkcs12 -export -inkey sectigo.key -in sectigo.pem -out sectigo.pfx\n```\n","isInternal":false,"tokens":52,"sizeBytes":177},{"name":"README.md","path":"packages/bruno-filestore/README.md","rawUrl":"https://raw.githubusercontent.com/usebruno/bruno/HEAD/packages/bruno-filestore/README.md","title":"bruno-filestore Documentation","category":"plugin-manifest","format":"markdown","content":"# Bruno Filestore\n\nA generic file storage and parsing package for Bruno API client.\n\n## Purpose\n\nThis package abstracts the file format operations for Bruno, providing a clean interface for parsing and stringifying Bruno requests, collections, folders, and environments.\n\n## Features\n\n- Format-agnostic APIs for file operations\n- Currently supports Bruno's custom `.bru` format\n- Designed for future extensibility to support YAML and other formats\n\n## Usage\n\n```javascript\nconst {\n  parseRequest,\n  stringifyRequest,\n  parseCollection,\n  stringifyCollection,\n  parseEnvironment,\n  stringifyEnvironment,\n  parseDotEnv\n} = require('@usebruno/filestore');\n\n// Parse a .bru request file\nconst requestData = parseRequest(bruContent);\n\n// Stringify request data to .bru format\nconst bruContent = stringifyRequest(requestData);\n\n// Example with future format support (not yet implemented)\nconst requestData = parseRequest(yamlContent, { format: 'yaml' });\n```\n\n### Writing a converted collection to disk\n\n`writeCollectionToDisk` takes a collection as returned by `@usebruno/converters` and writes it as a collection directory that Bruno can open:\n\n```javascript\nconst { openApiToBruno } = require('@usebruno/converters');\nconst { writeCollectionToDisk } = require('@usebruno/filestore');\n\nconst collection = openApiToBruno(openApiSpecification);\nawait writeCollectionToDisk(collection, './my-collection');\n```\n\n`outputDir` is created if it does not exist. If it already exists it must be empty, otherwise the call rejects with an error whose `code` is `'ENOTEMPTY'` and nothing is written.\n\n## API\n\nThe package provides the following functions:\n\n- `parseRequest(content, options = { format: 'bru' })`: Parse request file content\n- `stringifyRequest(requestObj, options = { format: 'bru' })`: Convert request object to file content\n- `parseCollection(content, options = { format: 'bru' })`: Parse collection file content\n- `stringifyCollection(collectionObj, options = { format: 'bru' })`: Convert collection object to file content\n- `parseFolder(content, options = { format: 'bru' })`: Parse folder file content\n- `stringifyFolder(folderObj, options = { format: 'bru' })`: Convert folder object to file content\n- `parseEnvironment(content, options = { format: 'bru' })`: Parse environment file content\n- `stringifyEnvironment(envObj, options = { format: 'bru' })`: Convert environment object to file content\n- `parseDotEnv(content)`: Parse .env file content\n- `writeCollectionToDisk(collection, outputDir, options = { format: 'yml' })`: Write a whole collection to `outputDir` in `yml` or `bru` format. Resolves to `outputDir`. `outputDir` must be missing or empty; otherwise it rejects with `code: 'ENOTEMPTY'` ","isInternal":false,"tokens":627,"sizeBytes":2705},{"name":"readme.md","path":"packages/bruno-graphql-docs/readme.md","rawUrl":"https://raw.githubusercontent.com/usebruno/bruno/HEAD/packages/bruno-graphql-docs/readme.md","title":"bruno-graphql-docs Documentation","category":"plugin-manifest","format":"markdown","content":"# bruno-graphql-docs\n\nStandalone graphql docs explorer module forked from [graphiql](https://github.com/graphql/graphiql)\n\n### Publish to Npm Registry\n```bash\nnpm publish --access=public\n```","isInternal":false,"tokens":49,"sizeBytes":190},{"name":"readme.md","path":"packages/bruno-js/readme.md","rawUrl":"https://raw.githubusercontent.com/usebruno/bruno/HEAD/packages/bruno-js/readme.md","title":"bruno-js Documentation","category":"plugin-manifest","format":"markdown","content":"# bruno-js\n\nProvides the script, test, vars and assert runtimes.\n\n### Publish to Npm Registry\n```bash\nnpm publish --access=public\n```","isInternal":false,"tokens":35,"sizeBytes":133},{"name":"readme.md","path":"packages/bruno-lang/readme.md","rawUrl":"https://raw.githubusercontent.com/usebruno/bruno/HEAD/packages/bruno-lang/readme.md","title":"bruno-lang Documentation","category":"plugin-manifest","format":"markdown","content":"# bruno-lang\n\nThe language utils for working with `.bru` files\n\n### Publish to Npm Registry\n```bash\nnpm publish --access=public\n```","isInternal":false,"tokens":34,"sizeBytes":131},{"name":"readme.md","path":"packages/bruno-query/readme.md","rawUrl":"https://raw.githubusercontent.com/usebruno/bruno/HEAD/packages/bruno-query/readme.md","title":"bruno-query Documentation","category":"plugin-manifest","format":"markdown","content":"# bruno-query\n\nBruno query with deep navigation, filter and map support\n\nEasy array navigation\n```js\nget(data, 'customer.orders.items.amount')\n```\nDeep navigation .. double dots\n```js\nget(data, '..items.amount')\n```\nArray indexing\n```js\nget(data, '..items[0].amount')\n```\nArray filtering [?] with corresponding filter function\n```js\nget(data, '..items[?].amount', i => i.amount > 20) \n```\nArray filtering [?] with simple object predicate, same as (i => i.id === 2 && i.amount === 20)\n```js\nget(data, '..items[?]', { id: 2, amount: 20 }) \n```\nArray mapping [?] with corresponding mapper function\n```js\nget(data, '..items..amount[?]', amt => amt + 10) \n```\n\n### Publish to Npm Registry\n```bash\nnpm publish --access=public\n```\n","isInternal":false,"tokens":206,"sizeBytes":724},{"name":"readme.md","path":"packages/bruno-schema/readme.md","rawUrl":"https://raw.githubusercontent.com/usebruno/bruno/HEAD/packages/bruno-schema/readme.md","title":"bruno-schema Documentation","category":"plugin-manifest","format":"markdown","content":"# bruno-schema\n\nThe schema definition for collections\n\n### Publish to Npm Registry\n```bash\nnpm publish --access=public\n```\n\n### Collection schema\n```bash\nid                       Unique id (when persisted to a db)\nuid                      Unique id\nname                     collection name\nitems                    Items (folders and requests)\n  |-uid                  A unique id   \n  |-name                 Item name \n  |-type                 Item type  (folder, http-request, graphql-request)\n  |-request              Request object\n    |-url                Request url\n    |-method             Request method\n    |-headers            Request headers (array of key-val)\n    |-params             Request params (array of key-val)\n    |-body               Request body object  \n      |-mode             Request body mode\n      |-json             Request json body\n      |-text             Request text body\n      |-xml              Request xml body\n      |-multipartForm    Request multipartForm body\n      |-formUrlEncoded   Request formUrlEncoded body\n```\n","isInternal":false,"tokens":206,"sizeBytes":1059},{"name":"README.md","path":"packages/bruno-sqlite/README.md","rawUrl":"https://raw.githubusercontent.com/usebruno/bruno/HEAD/packages/bruno-sqlite/README.md","title":"bruno-sqlite Documentation","category":"plugin-manifest","format":"markdown","content":"# @usebruno/sqlite\n\nSQLite storage for the Bruno API client. You author migrations as `.ts` and statements as `.sql`; the package compiles them into a typed data layer for the Electron main process.\n\n## Entry point\n\n`@usebruno/sqlite` (or `/node`) — main process only. Owns the DB and runs statements, using the built-in `node:sqlite`.\n\n## Add a migration\n\n```bash\nnpm run migration:new --workspace=packages/bruno-sqlite   # prompts for a name\n```\n\nThis scaffolds a sequence-prefixed `migrations/<seq>_<name>.ts` file exporting `up` and `down`. Each returns the SQL to apply or revert the migration (a plain string also works):\n\n```ts\nexport const up = (): string => {\n  return 'CREATE TABLE users (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, email TEXT);';\n}\nexport const down = (): string => {\n  return 'DROP TABLE users;';\n}\n```\n\nMigrations apply automatically (in prefix order) when the DB opens, and their content is hashed into a `_migrations` table so an already-applied migration can't be silently edited.\n\n### Verify migrations\n\n`npm run migration:verify` replays every migration against a throwaway `VACUUM INTO` copy of a real database, so it exercises them over actual data without touching the source DB. Point it at the DB with an absolute `DB_PATH`:\n\n```bash\ncp .env.example .env      # then set DB_PATH to an absolute path\nnpm run migration:verify --workspace=packages/bruno-sqlite\n```\n\nIt fails if a migration errors, or if a migration's content no longer matches what was recorded when it was first applied to that DB.\n\n### Schema\n\n[`SCHEMA.md`](./SCHEMA.md) shows the schema the migrations produce: an ER diagram plus each table's columns, keys and indexes. The pre-commit hook regenerates it whenever a migration is staged; `npm run schema` regenerates it by hand.\n\n## Add statements (sqlc syntax)\n\nWrite them in `statements/*.sql` — multiple per file. Each starts with a `-- name:` annotation, and params are named (`@param`):\n\n```sql\n-- name: list_users :many\nSELECT id, name, email FROM users ORDER BY name;\n\n-- name: get_user :one\nSELECT id, name, email FROM users WHERE id = @id;\n\n-- name: create_user :exec\nINSERT INTO users (name, email) VALUES (@name, @email);\n```\n\n| annotation | runs as | returns |\n|---|---|---|\n| `:one` | single row | row or `undefined` |\n| `:many` | rows | array |\n| `:exec` | write | `{ changes, lastInsertRowid }` |\n\nRun `npm run generate` (or `build`) to compile statements into the typed registry.\n\n## Use it — main process\n\n```js\nconst { createDatabase } = require('@usebruno/sqlite');\n\nconst { db, statements } = createDatabase('/path/to/bruno.db');\n\nstatements.execute('create_user', { name: 'Ada', email: 'ada@example.com' });\nconst users = statements.execute('list_users');       // no params\nconst ada   = statements.execute('get_user', { id: 1 });\n// db.close() on shutdown\n```\n\nParams are always an **object** keyed by the named parameters (no positional/array binding).\n\nThe package does not expose statements to a renderer. The host application registers its own IPC handler for each statement it wants to expose.\n\n## Files\n\nRows are a poor home for a large payload, so the package ships a `files` table with a store in front\nof it. Each entry is kept in whichever shape fits its size:\n\n| size | stored as |\n|---|---|\n| up to 1MB | a `BLOB` on the row |\n| over 1MB | a file next to the database, with the row pointing at it |\n\n`createDatabase` returns the store alongside the statements:\n\n```js\nconst { db, statements, files } = createDatabase('/path/to/bruno.db');\n\nconst entry = await files.write(buffer, { contentType: 'image/png' });  // { id, size, inline, contentType }\nconst bytes = await files.read(entry.id);                                // Uint8Array, or null\nfiles.stat(entry.id);\nawait files.remove(entry.id);\n```\n\n`read` returns the whole payload, from the row or from the spilled file.\n\nThe files directory defaults to `<database path minus extension>-files`; pass `filesDir` to place it\nelsewhere, and `inlineMaxBytes` to move the threshold.\n\n### Lifetime\n\nA file lives for as long as some row points at it. Declare that with a foreign key:\n\n```sql\nALTER TABLE runner_responses ADD COLUMN body_file_id INTEGER REFERENCES files(id);\n```\n\n`files.collect()` reads the foreign keys back off the schema, deletes every row no table references,\nremoves the files they own, and sweeps files on disk that no row names. Nothing has to be registered\nwith the store — adding the column is enough. Call it at startup.\n\nAs a safety rule, when *no* table references `files` the store leaves rows alone: with no referrers\nevery row would look unreferenced. The disk sweep still runs.\n\n### Reading files from the renderer\n\nThe package exposes no renderer API for files, just as for statements. Writes are main-process only; to let the renderer read a file, the host registers its own IPC handler for that read.\n\n## Development\n\n```bash\nnpm run build --workspace=packages/bruno-sqlite    # generate + bundle + types\nnpm run watch --workspace=packages/bruno-sqlite    # rebuild on change\nnpm run test  --workspace=packages/bruno-sqlite    # jest\n```\n\n`.sql` files are the only source of truth committed; `src/generated/` and `dist/` are git-ignored and produced by the build.\n","isInternal":false,"tokens":1280,"sizeBytes":5251},{"name":"readme.md","path":"packages/bruno-tests/collection_level_oauth2/readme.md","rawUrl":"https://raw.githubusercontent.com/usebruno/bruno/HEAD/packages/bruno-tests/collection_level_oauth2/readme.md","title":"collection_level_oauth2 Documentation","category":"plugin-manifest","format":"markdown","content":"# bruno-tests collection\n\nAPI Collection to run sanity tests on Bruno CLI.\n","isInternal":false,"tokens":16,"sizeBytes":75},{"name":"readme.md","path":"packages/bruno-tests/collection_oauth2/readme.md","rawUrl":"https://raw.githubusercontent.com/usebruno/bruno/HEAD/packages/bruno-tests/collection_oauth2/readme.md","title":"collection_oauth2 Documentation","category":"plugin-manifest","format":"markdown","content":"# bruno-tests collection\n\nAPI Collection to run sanity tests on Bruno CLI.\n","isInternal":false,"tokens":16,"sizeBytes":75},{"name":"readme.md","path":"packages/bruno-tests/collection/readme.md","rawUrl":"https://raw.githubusercontent.com/usebruno/bruno/HEAD/packages/bruno-tests/collection/readme.md","title":"collection Documentation","category":"plugin-manifest","format":"markdown","content":"# bruno-tests collection\n\nAPI Collection to run sanity tests on Bruno CLI.\n","isInternal":false,"tokens":16,"sizeBytes":75},{"name":"readme.md","path":"packages/bruno-tests/readme.md","rawUrl":"https://raw.githubusercontent.com/usebruno/bruno/HEAD/packages/bruno-tests/readme.md","title":"bruno-tests Documentation","category":"plugin-manifest","format":"markdown","content":"# bruno-tests\n\nThis package is used to test the Bruno CLI.\nWe have a collection that sits in the `collection` directory.\n\n### Test Server\n\nThis will start the server on port 80 which exposes endpoints that the collection will hit.\n\n```bash\n# install node dependencies\nnpm install\n\n# start server\nnpm start\n```\n\n### Run Bru CLI on Collection\n\n```bash\ncd collection\n\n# run collection against local server\nnode ../../bruno-cli/bin/bru.js run --env Local --output junit.xml --format junit\n\n# run collection against prod server hosted at https://testbench.usebruno.com\nnode ../../bruno-cli/bin/bru.js run --env Prod --output junit.xml --format junit\n```\n\n### License\n\n[MIT](LICENSE)\n","isInternal":false,"tokens":159,"sizeBytes":678},{"name":"README.md","path":"packages/bruno-tests/reports/README.md","rawUrl":"https://raw.githubusercontent.com/usebruno/bruno/HEAD/packages/bruno-tests/reports/README.md","title":"reports Documentation","category":"plugin-manifest","format":"markdown","content":"# Bruno CLI — Docker run reports\n\nThis folder is the bind-mount target for the Docker Compose example at [`../docker-compose.yml`](../docker-compose.yml).\n\nWhen you run:\n\n```bash\ncd packages/bruno-tests\ndocker compose run bruno-cli\n```\n\nthe container writes three report files into this directory:\n\n| File | Format | Purpose |\n|------|--------|---------|\n| `results.json` | JSON | Machine-readable run summary (request/response, timings, assertion results) |\n| `results.xml` | JUnit XML | For CI test reporters (GitHub Actions, GitLab CI, Jenkins, etc.) |\n| `results.html` | HTML | Human-readable report you can open in a browser |\n\nThe files themselves are gitignored — only this `README.md` and the `.gitignore` are tracked, which keeps the folder present in the repo so the `./reports:/reports` bind mount has somewhere to land without needing manual `mkdir` first.\n\nTo skip a format, drop the corresponding `--reporter-*` flag from the `command:` block in `docker-compose.yml`.\n","isInternal":false,"tokens":226,"sizeBytes":986}],"systemPromptSnippet":"<yakaai_skills repo=\"usebruno/bruno\">\n<!-- File: .claude/CLAUDE.md (Tokens: ~1035 | Category: claude-rule) -->\n# Project Guide\n\nBruno fork — open-source API client (Electron + React + Redux).\n\nBruno is an API **client**, not a server: it sends requests and handles responses (auth, params, headers, bodies, environments, cookies). Judge every behavior and edge case by \"what should a *client* do here?\" — surface malformed or hostile responses clearly and stay robust; don't enforce server-side correctness.\n\n## Quick Commands\n\n```bash\nnpm run dev              # Run electron + react concurrently\nnpm run dev:watch        # Same, with hot-reload of the electron main process\nnpm run storybook        # Component dev (bruno-app)\nnpm run lint:fix         # ESLint fix\nnpm test --workspaces    # Unit tests (Jest) across all packages\n```\n\nFull script list: root `package.json`. Setup: `nvm use && npm i --legacy-peer-deps && npm run setup` (Node **v22.12.0**, see `.nvmrc`).\n\n### Running a single test\n\n```bash\n# Single unit test (Jest) — scope to the workspace, pass a path/pattern after --\nnpm test --workspace=packages/bruno-app -- path/to/file.spec.js\nnpm test --workspace=packages/bruno-requests -- -t \"test name pattern\"\n\n# Single e2e test (Playwright) — a project flag is REQUIRED (config has no default)\nnpx playwright test tests/collection/create-collection.spec.ts --project=default\nnpx playwright test --project=default -g \"test name pattern\"\n```\n\nPrefer the smallest scope — one workspace, one spec — over `--workspaces` or the full e2e run.\n\n### Building shared packages\n\n`npm run dev` does **not** rebuild the shared packages (bruno-common, bruno-requests,\nbruno-filestore, bruno-sqlite, bruno-converters, bruno-query, bruno-schema-types, bruno-graphql-docs).\nAfter editing one, rebuild it or run its watcher, or the app won't pick up changes:\n\n```bash\nnpm run build:bruno-common      # also :bruno-requests, :bruno-filestore, :bruno-sqlite, etc.\nnpm run watch:common            # also watch:requests, watch:converters\n```\n\n## Key Architecture\n\n- **Main process entry**: `packages/bruno-electron/src/index.js`\n- **Renderer + Redux store**: `packages/bruno-app/src/providers/ReduxStore/`\n- Packages live under `packages/`, with top-level `tests/` and `playwright/`.\n\n## Testing\n\n- **Unit**: Jest, config per-package (`packages/*/jest.config.js`); bruno-app uses jsdom + `jest.setup.js`.\n- **E2E**: Playwright — fixtures, helpers, isolation rules, and pitfalls live in\n  `.claude/rules/testing.md`; use the `write-e2e-test` skill when adding specs.\n\n## Coding Standards\n\nFull list: `CODING_STANDARDS.md`. Mechanical style (indent, quotes, semicolons, no trailing\ncommas, arrow-param parens) is ESLint-enforced — run `npm run lint:fix`, don't hand-police it.\nNon-obvious project rules worth holding:\n\n- React: avoid `useEffect` (prefer derived state / custom hooks); import hooks by name, never `React.useX`; a component is controlled XOR uncontrolled; Tailwind for layout, styled-components `theme` for colors.\n- Every component owns a directory — `<ComponentName>/index.js(x)`, never `<ComponentName>.js(x)`.\n- Extract for readability or clear reuse; avoid only indirection that adds no payoff. Add `data-testid` attributes for Playwright selectors.\n\n## Detailed rules & references\n\nPath-scoped rules in `.claude/rules/` auto-attach when you touch matching files:\n`architecture.md` (`@usebruno/*` dependency boundaries, manifests), `bruno-app-layout.md` (bruno-app\nmodule layout), `electron-ipc.md` (IPC handlers + startup), `redux-store.md` (slices/middleware),\n`sqlite.md` (`@usebruno/sqlite` — migrations, statements, per-statement IPC), `testing.md` (e2e\npatterns & gotchas), `cross-platform.md` (Windows file/process/path pitfalls), `dsl-changes.md`\n(on-disk `.bru`/`.yml` format & backward compat), `feature-parity.md` (co-related features —\napp↔CLI twins, converter/shim/level/protocol pairs), `conventions.md` (readability, reuse,\nreplacement & pre-submit hygiene).\n\nRead on demand (not auto-loaded): `.claude/reference/architecture.md` — the monorepo map, request\npipeline, sandbox, core types, and dependency versions. Consult it before cross-package or\narchitectural work.\n\n<!-- File: .claude/rules/architecture.md (Tokens: ~863 | Category: anthropic-skill) -->\n---\npaths:\n  - \"packages/**/*\"\n---\n\n# Dependency Boundaries\n\nInternal `@usebruno/*` dependencies (from each `package.json`) form a strict DAG. **New code must\nrespect it** — a cyclic or upward dependency is an architectural bug, not a convenience.\n\nThe full monorepo map (build tools, request pipeline, sandbox, file formats, core data-model\ntypes, dependency versions) is the on-demand reference `.claude/reference/architecture.md` — read\nit before non-trivial cross-package or architectural work.\n\n## Dependency direction & ownership boundaries\n\n- **Leaf libs — zero internal `@usebruno/*` deps:** bruno-common, bruno-lang, bruno-query,\n  bruno-requests, bruno-graphql-docs, bruno-schema, bruno-schema-types, bruno-toml, bruno-sqlite\n  (a leaf, but *not* platform-neutral — see guardrail 6).\n- **Mid consumers:** bruno-js → (common, query); bruno-converters → (common, schema; schema-types\n  as devDep); bruno-filestore → (common, lang; schema-types as devDep).\n- **Top consumers (things flow *into* them, never out):** bruno-cli → (common, converters,\n  filestore, js, lang, requests); bruno-electron → (common, converters, filestore, js, lang,\n  requests, schema, sqlite); bruno-app → (common, converters, graphql-docs, schema).\n\nGuardrails this enforces:\n\n1. **bruno-common is the browser-safe base leaf.** It runs in the web renderer (`bruno-app`), not\n   just Node, so it must stay platform-neutral: no Node built-ins (`fs`, `path`, `os`, `crypto`,\n   `child_process`, `node:*`) and no dependency that itself pulls in Node. It currently ships with\n   zero runtime dependencies — keep it that way. It also depends on no other `@usebruno/*` package;\n   a util that needs Node or another bruno package belongs in a different package.\n2. **No shared/library package may import bruno-app or bruno-electron.** Renderer- or\n   Electron-specific code must not be pushed down into a lib to make it importable upward.\n3. **bruno-js must stay Electron-free.** It runs in both the Electron main process *and* the CLI\n   (bruno-cli → js). Adding an `electron`/IPC import to bruno-js breaks the CLI. Sandbox logic\n   belongs in bruno-js; host wiring belongs in bruno-electron.\n4. **bruno-schema-types is types-only** (a *devDependency* of converters/filestore, built with\n   plain `tsc`). Import types from it; never add runtime code or a runtime import of it.\n5. **bruno-schema (Yup) and bruno-schema-types (TS types) are distinct and both live.** app +\n   converters use `@usebruno/schema` for runtime validation; filestore + converters use\n   `@usebruno/schema-types` for compile-time types. A data-model change usually touches both.\n6. **bruno-sqlite is main-process only.** `@usebruno/sqlite` (= `/node`) imports `node:sqlite`,\n   `node:fs` and `node:crypto`; importing it from the renderer breaks the bundle. The renderer\n   reaches statements only through the per-statement IPC handlers in bruno-electron. See\n   `.claude/rules/sqlite.md`.\n\n## Declared dependencies must match real imports\n\nA package's `package.json` is the contract; workspace hoisting hides breaches of it. Declare every\nimport in the manifest of the package that imports it — one resolving only because a sibling or the\nroot happens to pull it in is a latent break. Test- and build-only packages belong in\n`devDependencies`; anything in `dependencies` ships to users with its transitive tree. Drop a\ndeclaration when the change removes its last consumer.\n\n<!-- File: .claude/rules/bruno-app-layout.md (Tokens: ~397 | Category: anthropic-skill) -->\n---\npaths:\n  - \"packages/bruno-app/**/*\"\n---\n\n# Module & Directory Layout (bruno-app)\n\nLayout is predictable enough here to be load-bearing, so a misplaced file is an architectural defect,\nnot a style nit — a breach blocks, the same as a dependency-direction violation in\n`.claude/rules/architecture.md`.\n\n- **A component owns a directory: `<ComponentName>/index.js(x)`, never `<ComponentName>.js`** — at\n  every nesting depth, including a sub-component of a component. Same shape for hooks\n  (`src/hooks/useX/index.js`) and `src/ui/` primitives.\n- **Everything a component owns lives in its directory** beside its `index.js` — `StyledWrapper.js`,\n  spec, component-specific utils. Reaching up into a parent's `StyledWrapper.js`, or leaving a\n  component's styles outside its directory, hands ownership of them to no one.\n- **`src/ui/` is a leaf**: app-agnostic presentational primitives only, and it must not import from\n  `src/components/` — the edge runs `components → ui`, never back. A `ui/` module needing one either\n  isn't a primitive, or its dependency belongs in `ui/` too.\n- **`utils/` is for stateless helpers you call.** A module *registered* into something — an editor\n  extension, a plugin, a node view, a keymap — is behavior or configuration, and filing it under\n  `utils/` hides that it's part of a schema or document contract. Give those their own directory.\n\nExisting breaches — bare `PascalCase.js(x)` component files, and `ui/MenuDropdown`'s import of\n`components/Dropdown` — are drift, not a second pattern. Don't extend them, and don't raise an\nuntouched one as a finding.\n\n<!-- File: .claude/rules/conventions.md (Tokens: ~1475 | Category: anthropic-skill) -->\n---\npaths:\n  - \"packages/**/*\"\n  - \"tests/**/*\"\n  - \"scripts/**/*\"\n---\n\n# Coding Conventions, Readability & Hygiene\n\n`CODING_STANDARDS.md` is the source of truth for Bruno's coding standards — read it. This file is\nthe judgment layer over those standards: the readability and code-hygiene calls a linter can't make.\nCode and comments must read as a natural, permanent part of the project — never as artifacts of the\ntask or session that produced them.\n\n## Style & formatting\n\nThe mechanical style — 2-space indent, single quotes (double for JSX/TSX attributes), semicolons,\nno trailing commas, parenthesized arrow params, spacing around arrows, no space before call parens\n— is enforced by ESLint and auto-fixed by `npm run lint:fix`. These deviations are real but\nlow-value to catch by hand: note them briefly rather than dwelling. Naming and casing that ESLint\ncan't mechanically repair still warrant attention.\n\n## Readability\n\n- **Descriptive names.** Functions and variables carry concise, descriptive names; an unclear or\n  misleading name is worth raising even when the code is otherwise correct.\n- **Reuse before you write.** Before adding a component, hook, or helper, search for the one that\n  already exists — by concept, not by the name you'd have picked — and read the nearest sibling\n  solving the same shape of problem, since its call site shows the intended composition. Reuse is\n  usually a *net deletion* — the bespoke markup and its CSS go. Where the existing primitive is\n  *almost* right, widen it rather than standing up a near-duplicate beside it; two near-identical\n  implementations diverge silently.\n- **Extraction & abstraction.** Extract a helper or shared abstraction whenever it genuinely\n  improves readability or serves a clear, anticipated reuse — this is encouraged and not gated on a\n  minimum number of call sites; suggest it where it would help. Avoid only *unnecessary* abstraction:\n  a layer that adds indirection without improving clarity or earning reuse — a generalized utility\n  built for a single site with no foreseeable second user, or options/config added \"for later.\"\n  Breaking a long, complex function into well-named local helpers for readability is always fine.\n- **Single-line indirection.** A one-line function that only forwards to another — adding a stack\n  frame without adding meaning — should be inlined.\n- **Optional chaining.** `?.` belongs only where the null case is handled right there (a fallback,\n  early return, or guard). Used elsewhere it hides whether a value can genuinely be null and works\n  against TypeScript's guarantees; fix the type or narrow first.\n- **Comments explain the why.** Genuinely complex flow deserves a comment covering the rationale an\n  obvious reading can't; self-explanatory code does not. Full bar in **Comments** below.\n- **Functional, but readable.** Prefer obvious, linear pipelines over deep functional machinery\n  (ADTs, monads) — the code should stay easy for any contributor to follow and extend.\n\n## Comments\n\n- **No situational or prompt-driven comments.** A comment must not reference the change, the task, or\n  the moment it was written. Drop `// added to fix ...`, `// as requested`, `// new logic for X`,\n  `// updated to handle ...`, `// per review`. If the reason genuinely matters, state it as a\n  timeless fact about the code (or link the issue/PR) — not as \"what I just did\".\n- **No obvious comments.** Don't restate what the code already says. `// loop over items` above a\n  loop, `// set the name` above `obj.name = name`, `// return the result` — these add nothing. If the\n  code is self-explanatory, leave it uncommented.\n- **Comment the why, not the what.** Reserve comments for what the code can't show: non-obvious\n  rationale, invariants, edge cases, a workaround and the constraint forcing it, units, or a pointer\n  to a spec/issue. These stay useful long after the change lands.\n- **No scaffolding or narration.** No `// ... existing code ...`, no placeholder or TODO-for-me\n  notes, no changelog or step-by-step narration in comments, no commented-out code left behind.\n\n## Beyond comments\n\n- **Anything added needs a live consumer in the same change.** No options, parameters, or\n  configuration \"for later\"; no payload field no reader destructures, branch for a state the producer\n  can't emit, or ignored parameter. Each is dead on arrival and reads as a contract honored somewhere\n  else — if the consumer is a follow-up, leave it out. Extracting a helper for readability or clear,\n  foreseeable reuse is fine (see **Readability → Extraction & abstraction** above).\n- Match the surrounding code's style and naming so a change is indistinguishable from the existing\n  codebase, not visibly bolted on.\n- Keep diffs minimal — no unrelated reformatting or whitespace churn. A drive-by cleanup worth doing\n  belongs in its own commit, and never in a *different package* than the change is about; interleaved,\n  reviewers can't separate scope creep from load-bearing edits.\n\n## Replacing code leaves nothing behind\n\nHalf a migration reads as a complete one — the leftovers survive review and mislead whoever edits the\nfile next. Closing the loop is part of the change.\n\n- **Follow what you replaced to every reference and remove it there** — the CSS whose selectors the\n  new markup no longer emits, the import whose last use just went, the prop nothing passes. Styles are\n  the easiest to leave behind and the most misleading when left: CSS for a class that renders nothing\n  fails silently and looks intentional.\n- **Don't copy a block to its new home and leave the original.** Where both copies must genuinely\n  exist — separate call sites, or one rule enforced on both sides of a process boundary — name the\n  invariant in a comment at both, because nothing else keeps them in sync.\n- **Reconcile the whole flow, not the entry point.** Reshaping something that crosses a boundary — a\n  payload, a serialized field, a lookup key — means walking to every consumer on the far side.\n  Deleting a control (a sanitizer, a validation or escaping step) is a decision to state, not a side\n  effect of a rewrite.\n\n## Before you call it done\n\n- **Run the affected workspace's tests**, not just the specs you wrote.\n- **Changed a return value or payload shape?** Other specs assert it too — exact-equality assertions\n  elsewhere break on an added key.\n- **Walk the acceptance criteria against the diff one at a time.** Compound criteria are what slip:\n  the half of the sentence you didn't have open never got exercised.\n\n<!-- File: .claude/rules/cross-platform.md (Tokens: ~678 | Category: anthropic-skill) -->\n---\npaths:\n  - \"scripts/**/*\"\n  - \"packages/bruno-electron/**/*\"\n---\n\n# Cross-Platform Guidelines\n\nThis is an Electron app targeting macOS, Windows, and Linux. All code touching the filesystem, child processes, signals, or paths must work on all three platforms.\n\n## Filesystem\n\n- `fs.rmSync` with `force: true` only suppresses `ENOENT`, NOT `EPERM`/`EBUSY`. Windows locks files aggressively (antivirus, indexing, open handles). Always use `maxRetries` and `retryDelay` when deleting directories.\n- Use `path.join()` / `path.resolve()` for all paths — never hardcode `/` separators.\n- Windows paths are case-insensitive. Use `normalizePath()` (from `utils/common/path`) when comparing collection/workspace paths.\n- `app.getPath()` returns platform-specific directories (`userData`, `documents`, etc.). Never assume POSIX-style locations.\n- File watchers (chokidar) may emit different event orderings across platforms. Don't depend on specific add/change/unlink sequences.\n\n## Child Processes\n\n- `spawn('npm', [...])` requires `shell: true` on Windows because `npm` is `npm.cmd`.\n- `child.kill()` with `shell: true` only kills the shell wrapper (`cmd.exe`) on Windows, leaving orphaned children. Use `taskkill /pid <pid> /T /F` to kill the entire process tree on Windows.\n- `execSync` / `spawn` commands must avoid Unix-only syntax (`&&` chains work in cmd.exe, but pipes and redirects differ).\n\n## Signals & Shutdown\n\n- `SIGINT` / `SIGTERM` are unreliable on Windows with `shell: true`. Also handle `SIGHUP` as fallback.\n- App shutdown must close all file watchers. Each watcher class implements `closeAllWatchers()`, orchestrated by `closeAllWatchers()` in `index.js`.\n\n## Line Endings\n\n- Files authored on Windows use CRLF. When parsing multiline `.bru`/text blocks line by line, split with a CRLF-aware regex (`/\\r\\n|\\r|\\n/`), never on `\\n` alone — a trailing `\\r` otherwise leaks into parsed values and causes spurious dirty-state and diffs. the `.split(/\\r\\n|\\r|\\n/)` in `bruno-lang`'s `v2/src/envToJson.js` is the reference pattern; keep new parsers consistent with it.\n\n## stdout vs stderr\n\n- Dev tools (rsbuild, webpack, electron-builder) may route startup output to stderr on Windows. When detecting patterns in process output, check both streams.\n\n## Platform-Specific Dependencies\n\n- `forceInstallPlatformDeps()` in `scripts/setup.js` installs platform-specific native modules (e.g., `@lydell/node-pty-{platform}-{arch}`).\n- Electron builder config handles platform-specific packaging (`mac`/`win`/`linux` targets).\n\n## Path Separators in Collection/Workspace Stores\n\n- electron-store persists paths as-is. A workspace opened on macOS stores `/Users/...`, on Windows `C:\\Users\\...`.\n- `ELECTRON_USER_DATA_PATH` only applies when `isDev` is true (`index.js:22`).\n\n<!-- File: .claude/rules/dsl-changes.md (Tokens: ~3170 | Category: anthropic-skill) -->\n---\npaths:\n  - \"packages/bruno-app/**\"\n  - \"packages/bruno-electron/**\"\n  - \"packages/bruno-cli/**\"\n  - \"packages/bruno-lang/**\"\n  - \"packages/bruno-filestore/**\"\n  - \"packages/bruno-schema/**\"\n  - \"packages/bruno-schema-types/**\"\n  - \"packages/bruno-converters/**\"\n---\n\n# On-Disk DSL & Serialization Changes\n\nBruno persists everything users create — collections, requests, environments, folders, and\nconfig — as plain-text files on the user's disk and in their Git repos. Two on-disk formats\nexist:\n\n- **`.bru`** — the Bru DSL. Read/write goes through the **v2 (ohm-js)** grammar in\n  `packages/bruno-lang/v2/src`, via `packages/bruno-filestore/src/formats/bru`\n  (`bruToJsonV2` / `jsonToBruV2`). Make all `.bru` changes in v2.\n- **`.yml`** — OpenCollection YAML, handled by `packages/bruno-filestore/src/formats/yml`. This is\n  the current `DEFAULT_COLLECTION_FORMAT` (canonical: `packages/bruno-filestore/src/constants.ts`,\n  duplicated for the renderer in `packages/bruno-app/src/utils/common/constants.js`), so new\n  collections are `.yml` unless the user chooses otherwise.\n- Plus `bruno.json` (per-collection config, passed through `stringifyCollection`) and `.env` files\n  (`parseDotEnv` / `dotenvToJson`).\n\n(`packages/bruno-toml` exists but is **orphaned** — imported nowhere outside its own test and on no\nserialization path; a DSL change never needs it. `preferences.json` is app-level electron-store\nstate under `userData`, not part of a collection's on-disk contract, so it is out of scope here.)\n\nThese files outlive the version that wrote them: an older Bruno reads files written by a\nnewer one, teammates on different versions share the same repo, and users hand-commit these\nfiles. **A change to the on-disk shape is a change to a public contract.** Get it wrong and\nyou break parsing, corrupt collections, or silently drop user data — on collections you\ncan't reach to fix.\n\nThe in-memory objects that become these files are often assembled far from the serialization\nlayer — in `bruno-electron` (IPC handlers) or `bruno-app` (Redux) — and handed to\n`bruno-filestore`, which can't tell that a field's shape changed upstream. So a DSL-affecting\nchange can originate in *any* package on the data path, not just the format packages. Treat\nthis rule as in scope for changes anywhere the collection/request/environment/config object\nis built, mutated, or serialized — the serialization packages plus `bruno-app`,\n`bruno-electron`, and `bruno-cli`, where these objects are assembled or read/written. Adding a\npersisted field in app/electron without wiring it through `bruno-filestore` (both formats) and\n`bruno-schema` silently drops or rejects data on save — the exact bug this rule guards against.\n\n## Rules\n\n**Avoid unnecessary DSL changes.** First ask whether the need can be met in memory, in the\nUI, or as a derived value without touching the serialized shape at all — most enhancements\ncan. Only change the on-disk format when the data genuinely must persist.\n\n**Additive and optional only.** A new field must be optional with a safe default, so files\nwithout it still parse and behave. Never rename, remove, repurpose, or retype an existing\nfield, and never change its semantics — old files and old app versions depend on the\ncurrent meaning.\n\n**Round-trip must be lossless.** `parse(stringify(x))` must equal `x`, and `stringify` must\nnever drop fields it doesn't recognize. A file written by a newer version, opened and\nre-saved by an older one, must not lose the newer fields.\n\n**Prove escaping on the written bytes, not in memory.** When a value is embedded in a\ndelimited block (`.bru` multiline `'''…'''`, annotation args), an in-memory `escape`/`unescape`\nround-trip can pass while the on-disk file still corrupts — the parser terminates at the first\nre-introduced delimiter. Test `parse(read(serialize(x)))` on the actual bytes, and fuzz across\nquote/backslash runs. Escape at single-character granularity: escaping a multi-character\ndelimiter by splitting on the whole delimiter (`split(\"'''\")`) leaves fragments that recombine\non runs of length ≥ the delimiter; escape the backslash first, then every single delimiter char.\nRef: `escapeMultilineDescription` in `bruno-lang/v2/src/utils.js`.\n\n**New syntax breaks older parsers — not just older fields.** The additive-and-optional rule\nprotects field-level compat, but adding new `.bru` *syntax* (a new escape form, block delimiter,\nor annotation grammar) makes files written by the new version fail to *parse* in older versions,\nwhich have no handling for it. This is a forward-compat break beyond field loss; it needs an\nexplicit decision and a compatibility path before shipping.\n\n**Descriptions are string-only internally.** Every inline `description` field (headers, params,\nassertions, variables, body entries, ...) is stored as a plain `string`. On the **yml** side,\nparsers accept a bare string *or* a legacy `{ content }` object on read and normalize to the\nstring (`formats/yml/common/{headers,variables,assertions,actions}.ts`, `parseEnvironment.ts`,\n`body.ts`); writers emit a bare string. This read-accepts-object / write-emits-string asymmetry is\nan established convention — not a lossy round-trip — proven by `formats/yml/common/variables.spec.ts`.\nThe `.bru` side has no `{ content }` handling; it stores descriptions as annotation/textblock\nstrings straight from the grammar. New inline description-like fields should follow this\nstring-only shape.\n\n*Exception — `docs` is not string-only.* Collection/folder documentation (`docs`) is written by\nthe **yml** layer as an **object** `{ content, type: 'text/markdown' }`\n(`formats/yml/stringifyCollection.ts`, `stringifyFolder.ts`) and normalized back to a string on\nread; the `.bru` side writes it as a plain textblock. So \"follow the string-only shape\" applies to\ninline `description`, *not* to `docs`-style documentation fields — check which family a new field\nbelongs to before copying either pattern.\n\n**Keep both formats in lockstep.** Any field added or changed must be handled in *both*\n`bru` and `yml` — parse and stringify on each side — or the same collection behaves\ndifferently depending on its format. There is **no dedicated `bru`↔`yml` converter**: a request\nmoves between formats by being parsed in its source format to the shared in-memory object and\nre-stringified in the target format (see `SaveTransientRequest` passing `sourceFormat` +\n`targetFormat` to `renderer:save-transient-request`). So a field handled in only one format is\n**silently dropped** the moment a request is copied/saved from a `.bru` collection into a `.yml`\none (or vice-versa). Pay attention to the *unset* case: a field that defaults or persists one way\nin `.bru` and another in `.yml` (e.g. one path returns `''`, the other `'1'`) is a divergence bug.\nThe default app-data workspace and custom-filesystem workspaces are a second such twin — a value\nthat persists in one but is silently dropped in the other is the same class of bug. Update every\nlayer the field touches: `bruno-schema-types` (types), `bruno-schema` (Yup validation),\n`bruno-filestore` (both formats), `bruno-converters` (import/export), and the grammar in\n`bruno-lang` **v2** if `.bru` syntax itself changes.\n\n**`bruno-schema` (Yup) is not optional — it will reject your field.** The collection/request/\nenvironment Yup schemas are declared `.noUnknown(true).strict()`, and they are validated on the\n**save path** (e.g. `itemSchema.validate` / `environmentSchema.validateSync` in\n`bruno-app`'s `slices/collections/actions.js` and `bruno-electron`'s `store/global-environments.js`).\nA new serialized field that round-trips through filestore but is **not** added to\n`packages/bruno-schema` will throw a validation error on save. `bruno-schema` (runtime Yup) and\n`bruno-schema-types` (TS types) are distinct and both must be updated.\n\n**When a shape must change, migrate on read — never make users edit files.** Add a\nread-time compatibility shim that upgrades the old shape as it is parsed, following the\nexisting patterns: `ensureAuthV3Rc1BackwardsCompatibility` in `formats/yml/parseItem.ts`,\nand the pre-v3 status/statusText swap in `formats/bru/index.ts`. Gate any eventual removal\nbehind a major version bump and leave a dated `TODO(remove after vN)`.\n\n**Design for scale and consistency.** New keys follow the naming and nesting of the existing\n`meta`/section blocks (`meta.name`, `meta.seq`, `meta.type`, `meta.tags`, ...). Prefer\nstructures that extend cleanly — a keyed list over a positional one, an object over a\nwidening union — and match how neighbouring fields are already modelled. Note the on-disk\n`meta {}` block does **not** map 1:1 to the type: in `bruno-schema-types` those keys are flattened\nonto `Item` (`seq`, `name`, `type`, `tags`, `description` directly on the item), and the v2 ohm\ngrammar treats `meta` as an open dictionary — the four names above are the *modeled* ones, not a\nclosed set.\n\n**Name properties like they're permanent — because they are.** A DSL key is written once and\neffectively forever: renaming it breaks every existing file and forces a compat shim. These\nnames are also user-facing — people read and hand-edit `.bru`/`.yml` — and are read by AI\nagents reasoning over collections. Choose clear, descriptive, fully spelled-out names, with\nno cryptic abbreviations or ambiguity, consistent with existing keys, and get it right the\nfirst time. A property name deserves stricter scrutiny than an ordinary variable.\n\n**Test the contract.** Add round-trip tests (parse → stringify → parse) for the new field in\n*both* formats, plus a golden fixture of the *old* format that must still parse unchanged. The\npaired `parseItem.spec.ts` / `stringifyItem.spec.ts` (and `parse*`/`stringify*` for collection,\nfolder, environment) next to each serializer in `bruno-filestore/src/formats/yml` are the pattern;\n`common/variables.spec.ts` and `common/datatype.spec.ts` show field-level cases.\n\n## A new persisted field — the files it usually touches\n\nAdding a single field tends to span several packages (a `.bru`-centric change lands mostly in\n`bruno-lang/v2`):\n\n- `bruno-lang/v2/src` — the `.bru` entry points: `bruToJson.js`, `collectionBruToJson.js`,\n  `envToJson.js`, `jsonToBru.js`, `jsonToCollectionBru.js`, `jsonToEnv.js`, plus `utils.js` for\n  shared helpers.\n- `bruno-filestore/src/formats/yml` — parse **and** stringify for the `.yml` side.\n- `bruno-schema/src/collections/index.js` — the Yup schema (skip this and it fails on save).\n- `bruno-schema-types/src/collection/item.ts` — the TypeScript type.\n- `bruno-converters` — a **separate field list from `bruno-filestore`**, not the same code path:\n  `src/opencollection/items/{http,graphql,grpc,websocket}.ts` maps the field for the OpenCollection schema, might have typescript issues to be consulted with https://github.com/opencollection-dev/opencollection when needed\n  export *and* import, and `src/postman/bruno-to-postman.js` for Postman export. A field wired\n  through filestore but not here round-trips on disk and is silently lost on export.\n- `bruno-app/src/utils/collections/index.js` — `transformRequestToSaveToFilesystem` and siblings\n  whitelist fields before save; a field missing here never reaches the serializer.\n- `bruno-electron/src/ipc/yml-migration.js` — BRU → YML migration (bru parse → yml stringify, plus\n  `bruno.json` → `opencollection.yml` config transforms); verify the field survives migration.\n- the collection utils in `bruno-electron` / `bruno-app` that build the object.\n- Round-trip specs next to the serializers (`formats/yml/parseItem.spec.ts` + `stringifyItem.spec.ts`).\n\nFind a comparable field already in the code and follow how it's wired before adding yours.\n\n## Before changing the DSL — checklist\n\n- [ ] Change is genuinely necessary (can't be solved in memory / UI)\n- [ ] New field is optional with a safe default; nothing renamed, removed, or retyped\n- [ ] Handled in both `bru` and `yml`, parse **and** stringify\n- [ ] Types (`bruno-schema-types`), Yup schema (`bruno-schema`), and converters updated —\n      \"converters\" means `bruno-converters/src/opencollection/items/*` in **both** directions,\n      which is a different field list from `bruno-filestore`\n- [ ] Field survives the app save transform and BRU → YML migration (see `feature-parity.md` §2)\n- [ ] Old files still parse — read-time compat shim added if the shape changed\n- [ ] No new `.bru` syntax that older parsers can't read (or a forward-compat path decided)\n- [ ] Any new escaping proven via on-disk reparse + fuzz, escaping single chars not the delimiter\n- [ ] `bru`/`yml` (and default vs custom workspace) behave identically, including the unset case\n- [ ] Round-trip + old-format fixture tests added\n- [ ] Naming/structure consistent with existing blocks and scalable\n\n<!-- File: .claude/rules/electron-ipc.md (Tokens: ~2656 | Category: anthropic-skill) -->\n---\npaths:\n  - \"packages/bruno-electron/**/*\"\n---\n\n# Electron Main Process Guide\n\nThe main process (`packages/bruno-electron/src/index.js`) owns the filesystem, network, child\nprocesses, and all electron-store data. The renderer reaches it **only** over IPC through the\npreload bridge — there is no direct Node access in the renderer despite `nodeIntegration: true`\n(see Security below). Before adding or changing any IPC, read this file plus the target\n`src/ipc/<domain>.js` handler.\n\n## Preload bridge & renderer access\n\n`src/preload.js` is the *only* surface the renderer sees. It exposes, via\n`contextBridge.exposeInMainWorld('ipcRenderer', …)`, exactly:\n\n- `invoke(channel, ...args)` → `ipcRenderer.invoke` (renderer→main request/response)\n- `send(channel, ...args)` → `ipcRenderer.send` (fire-and-forget)\n- `on(channel, handler)` → subscribe; **returns an unsubscribe function** and **strips the\n  Electron `event`/`sender` arg** — renderer handlers receive `(...args)`, not `(event, ...args)`\n- `getFilePath(file)` (via `webUtils.getPathForFile`) and `openExternal(url)` (via `shell.openExternal`)\n\nAlso exposed: `window.isPlaywright` (`process.env.PLAYWRIGHT === 'true'`).\n\nRenderer code accesses this as `window.ipcRenderer` (e.g. `const { ipcRenderer } = window;` in\n`bruno-app/src/providers/App/useIpcEvents.js`). **There is no channel allowlist** — any channel\nstring is forwarded. Do not rely on the preload to gate channels; validate inputs inside the\nhandler.\n\n## Channel naming convention\n\nFollow the prefix that matches the direction — the vast majority of handlers already do:\n\n- **`renderer:*`** — renderer→main via `ipcMain.handle` (the default for new work; ~255 of ~290\n  handlers). e.g. `renderer:ready`, `renderer:save-request`, `renderer:create-workspace`,\n  `renderer:browse-directory`, `renderer:get-global-environments`.\n- **`main:*`** — main→renderer push via `webContents.send`. e.g. `main:load-preferences`,\n  `main:collection-opened`, `main:workspace-opened`, `main:collection-tree-updated`,\n  `main:display-error`, `main:app-loaded`. A few `main:*` names are *internal* main-process\n  events routed through `ipcMain.emit`/`ipcMain.on` (`main:renderer-ready`,\n  `main:workspaces-ready`) — see the startup sequence.\n- **Domain-namespaced invokes** for stateful sub-protocols: `grpc:*`, `renderer:ws:*` (WebSocket),\n  `terminal:*`, `menu:*` (menu→main), `internal:*` (main-to-main only).\n- **Legacy unprefixed network channels** — `send-http-request`, `cancel-http-request`,\n  `fetch-gql-schema`, `clear-oauth2-cache`, `connections-changed`. These predate the convention;\n  prefer a `renderer:*` name for new request-side channels rather than adding to this unprefixed set.\n\n## Handler registration\n\nAll handlers are registered inside `app.on('ready')` in `index.js` (the `// register all ipc\nhandlers` block) by calling per-domain `register*Ipc(mainWindow, …)` functions, each `require`d\nat the top of `index.js`. `registerPreferencesIpc` (which kicks off onboarding + the\n`renderer:ready` handler) runs *before* `registerWorkspaceIpc` (which owns `main:renderer-ready`);\nthis is safe because `renderer:ready` only fires after the renderer mounts, by which point every\nhandler is registered. **A new IPC domain must add its own `register*Ipc` call here** — a handler\nfile that is never `require`d + called registers nothing.\n\n## Where new handler code goes — keep it small and testable\n\n**Don't let a handler file keep growing to where its size hurts readability and maintainability.**\nWhen a group of handlers forms a separable feature, give it its own `ipc/<domain>.js` with its own\n`register*Ipc` (wired into `index.js` as above) rather than appending to an existing large file —\nseparate it whenever it makes sense. Splitting by domain is already the norm here (`apiSpec.js`,\n`workspace.js`, `mount.js`, …).\n\n**Structure a handler so its callback is unit-testable.** Write the logic as a named, exported\nfunction and let the thin `ipcMain.handle(channel, fn)` line just call it — not an inline anonymous\narrow, which can only be exercised through Electron. The exported callback can then be tested\nwithout spinning up the main process: see `ipc/swagger-fetch.js` (`proxySwaggerFetch`) and the\nspecs under `ipc/network/` and `ipc/ai/`.\n\n## Error-handling convention\n\nTwo patterns by handler type — match the one that fits:\n\n- **CRUD / lifecycle handlers throw or reject.** Validation failures use bare\n  `throw new Error(...)`; caught errors are re-surfaced with `return Promise.reject(error)` or\n  `throw error`. The renderer `invoke` promise rejects and is shown via a `main:display-error`\n  toast (`useIpcEvents.js`). A few read-only probes intentionally swallow and return a fallback\n  (e.g. `exists-sync` → `false`, `get-collection-workspaces` → `[]`).\n- **Request-execution handlers embed the error in the result.** `send-http-request`\n  (`ipc/network/index.js`) does **not** reject on a failed request/script/test — it returns a\n  result object carrying an `error` field so the renderer can render the failure as data. New\n  network handlers follow this embed-in-result shape; new CRUD handlers throw/reject.\n\n## Security constraints\n\n`BrowserWindow` `webPreferences` (in `index.js`): `nodeIntegration: true`,\n`contextIsolation: true`, `preload: preload.js`, `webviewTag: true`. **This `nodeIntegration:\ntrue` + `contextIsolation: true` combination is intentional — do not \"fix\" it.** No `sandbox`\nkey (defaults off); `webSecurity` defaults on. A CSP is set via `electron-util`'s\n`setContentSecurityPolicy` in `index.js` (`default-src 'self'`; `connect-src` adds PostHog;\n`script-src 'self' data:`; `img-src` allows http/https/blob/data). `form-action 'none'` is\ndeliberately commented out to keep OAuth2 working. External navigation is hardened via\n`will-redirect` + `setWindowOpenHandler`, which route http/https to `shell.openExternal` and deny\neverything else — preserve this when touching window/navigation code.\n\n## IPC startup sequence\n\n```\nrenderer mounts\n  -> useEffect: ipcRenderer.invoke('renderer:ready')    [useIpcEvents.js]\n     -> main handles 'renderer:ready'                    [ipc/preferences.js]\n          await onboardingPromise\n          send main:load-preferences\n          send main:load-global-environments (+ main:git-version, reopen last apispecs, ...)\n          ipcMain.emit('main:renderer-ready')\n            -> workspace.js listener:                     [ipc/workspace.js]\n                 await ensureDefaultWorkspaceExists()\n                 send main:workspace-opened (per workspace)\n                 send + ipcMain.emit('main:workspaces-ready')\n                   -> onboarding.js sends main:collection-opened  [app/onboarding.js]\n\ndid-finish-load (independent, from Chromium):                     [index.js]\n  -> wraps webContents.send with a safeStringify/safeParse JSON roundtrip\n  -> send main:app-loaded\n     -> renderer sink sets data-app-state=\"loaded\"        [pages/Bruno/index.js, NOT useIpcEvents.js]\n```\n\n## IPC race-condition pattern\n\nWhen `ipcMain.emit` triggers multiple `ipcMain.on` listeners:\n- Sync listeners complete fully before the next starts.\n- Async listeners yield at the first `await`, letting the next listener start.\n- `webContents.send` from earlier listeners can arrive at the renderer before messages from\n  later async continuations.\n\nFix: chained events (`main:renderer-ready` → `main:workspaces-ready` → deferred\n`main:collection-opened`) enforce ordering. Reuse this pattern rather than adding `setTimeout`s.\n\n## Persistent stores (`src/store/`, electron-store)\n\nStores are electron-store JSON files under `app.getPath('userData')`, one module per file —\n**read the directory for the current set** (it is large: preferences, cookies, oauth2,\ncollection-security, secrets, workspace/global environments, last-opened-*, window-state, …).\nGotcha: multiple modules can share the *same* JSON file. `lastOpenedCollections` lives in\n`preferences.json` but is owned by `store/last-opened-collections.js` (a distinct\n`new Store({ name: 'preferences' })` module), **not** by `store/preferences.js`. Don't assume a\nstore key belongs to the module named after the file.\n\n## File watchers (`src/app/`)\n\nWatchers live in `src/app/` — **read the directory for the current set** (`collection-watcher.js`,\n`workspace-watcher.js`, `apiSpecsWatcher.js`, `dotenv-watcher.js`). Cleanup is\n**not** uniform:\n- `closeAllWatchers()` in `index.js` tears down exactly three —\n  `collectionWatcher`, `workspaceWatcher`, `apiSpecWatcher` — each of which exposes\n  its own `closeAllWatchers()`.\n- `dotenv-watcher.js` is a shared singleton exposing `closeAll()` and is torn down transitively by\n  the workspace watcher (and driven by the collection watcher), **not** from `index.js`.\n\nWhen adding a watcher, decide which lifecycle it belongs to and register its cleanup in the\nmatching place — a top-level watcher goes into `closeAllWatchers()` in `index.js`; a\nper-workspace/per-collection one hangs off the owning watcher.\n\n## Onboarding flow\n\nEntry: `src/app/onboarding.js` (`onboardUser`), invoked synchronously when `registerPreferencesIpc`\nruns.\n\n1. Checks `preferencesUtil.hasLaunchedBefore()`.\n2. New users: imports the sample collection from `resources/data/sample-collection.json`.\n3. Defers `main:collection-opened` (via a pending-collection var) until `main:workspaces-ready`.\n4. `DISABLE_SAMPLE_COLLECTION_IMPORT === 'true'` skips the import (set in Playwright).\n\n## Key environment variables\n\n- `ELECTRON_USER_DATA_PATH` — custom userData path, **dev mode only** (`index.js:22`, guarded by `isDev`).\n- `DISABLE_SAMPLE_COLLECTION_IMPORT` — skip sample collection on first launch.\n- `PLAYWRIGHT` — signals the app is running under Playwright (read in `preload.js`, stores, etc.).\n- `DISABLE_SINGLE_INSTANCE` — allow multiple app instances.\n\n(All four are set to `'true'` for e2e in `playwright/index.ts`.)\n\n## Before adding or changing an IPC handler — checklist\n\n- [ ] Handler added in `src/ipc/<domain>.js` as `ipcMain.handle('renderer:<verb>', …)` (or the\n      matching domain prefix); no new unprefixed channel.\n- [ ] New handler lives in a focused `ipc/<domain>.js` — a separable feature gets its own file\n      rather than bloating an existing one; its callback is a named exported function\n      (unit-testable), not an inline arrow.\n- [ ] If it's a new domain, its `register*Ipc(mainWindow, …)` is called in `index.js`'s ready block.\n- [ ] Inputs validated inside the handler (the preload has no allowlist).\n- [ ] Error path matches the handler type — CRUD throws/rejects; request-execution embeds `error`\n      in the result.\n- [ ] For a `main:*` push channel, the renderer adds a listener **and** an unsubscribe in\n      `useIpcEvents.js`'s cleanup.\n- [ ] Cross-process values survive the `did-finish-load` JSON roundtrip (no functions,\n      class instances, or circular refs in payloads).\n\n<!-- File: .claude/rules/feature-parity.md (Tokens: ~2077 | Category: anthropic-skill) -->\n---\npaths:\n  - \"packages/**/*\"\n---\n\n# Co-Related Features & Parity Surfaces\n\nBruno implements the same behaviour more than once — app vs CLI, `.bru` vs `.yml`, filestore vs\nconverters, implementation vs QuickJS shim. **A change to one side of a pair is not a finished\nchange**: the other side silently keeps the old behaviour and ships. The symptom is never a broken\nbuild — it's a feature that works in the app but not in `bru run`, a field that survives save but\nvanishes on export or migration, a `bru.*` method that is `undefined` in the default sandbox.\n\nThis rule maps *which* surfaces pair up. `.claude/rules/dsl-changes.md` owns the on-disk contract\nin depth.\n\n## When you work on a change\n\n1. **Find the twin before writing the fix.** For every file you touch in the tables below, open\n   its counterpart.\n2. **Update the twin in the same change** when it's within what was asked.\n3. **Otherwise, say so explicitly.** If the twin is out of scope, doesn't apply, or you're unsure,\n   list it in your reply to the user (and in the PR description) with the path and why it was left\n   — never leave a one-sided change unmentioned.\n4. **Prefer unifying over copying.** If the logic has no Electron- or CLI-only dependency, move it\n   into `@usebruno/requests` / `@usebruno/common` (respecting `.claude/rules/architecture.md`)\n   instead of patching twice. Precedent: `utils/cookies.js` is one line in each runtime —\n   `module.exports = require('@usebruno/requests').cookies;`. Where both copies must stay, note the\n   pairing in a comment at both sites.\n\n## 1. App ↔ CLI — highest priority\n\n`bruno-cli` is how collections run in CI. Several pipeline modules are **copied, not shared**, and\na bug fixed in the app is still present in `bru run` until its twin is fixed. Trace app-reported\nbugs to the shared logic, not the reporting surface.\n\n| Concern | `bruno-electron/src/…` | `bruno-cli/src/…` |\n|---|---|---|\n| Request execution — pre/post scripts, vars, asserts, tests, runner flow (`setNextRequest`, `skipRequest`) | `ipc/network/index.js` | `runner/run-single-request.js`, `commands/run.js` |\n| Request assembly | `ipc/network/prepare-request.js` | `runner/prepare-request.js` |\n| HTTP client, TLS, certs | `ipc/network/axios-instance.js` | `utils/axios-instance.js` |\n| Interpolation | `ipc/network/interpolate-{vars,string}.js` | `runner/interpolate-{vars,string}.js` |\n| Collection tree, `merge{Headers,Scripts,Vars,Auth}` | `utils/collection.js` | `utils/collection.js` |\n| Collection loading, `bruno.json` / `opencollection.yml` config | `utils/collection-reader.js`, `app/collection-watcher.js` | `utils/collection.js` (`getCollectionConfig`) |\n| Environments & persisted vars | `utils/environments.js` | `utils/environment.js`, `utils/persist-variables.js` |\n| Proxy · OAuth 2.0 · form-data · filesystem | `utils/{proxy-util,oauth2,form-data,filesystem}.js` | `utils/{proxy-util,oauth2,form-data,filesystem}.js` |\n| AWS SigV4 | `ipc/network/awsv4auth-helper.js` | `runner/awsv4auth-helper.js` |\n\n- A new app runner option (tags, delay, bail) needs a `bru run` flag, or a stated reason.\n- Import: the app imports Postman, Insomnia, OpenAPI, WSDL and OpenCollection; `bru import`\n  (`commands/import.js`) only OpenAPI and WSDL. Changes to a shared converter affect both.\n- **Known-divergent by design.** The CLI has no preferences store (so no `resolveInheritedSettings`),\n  no Electron window, no system-browser OAuth, and no `@usebruno/schema` validation. Its runner\n  supports only `http-request` and `graphql-request`: it has no gRPC or WebSocket transport, and\n  every request item goes through the HTTP pipeline in `runner/run-single-request.js`.\n\n## 2. Persisted fields — every layer that carries a field list\n\nA field is added in one place and read in many. Each of these keeps its **own** list, aligned by\nhand:\n\n| Layer | Where | Missed → |\n|---|---|---|\n| Types | `bruno-schema-types/src/collection/item.ts` | type drift |\n| Yup schema | `bruno-schema/src/collections/index.js` — `.noUnknown(true).strict()` | **throws on save** |\n| App save transform | `bruno-app/src/utils/collections/index.js` — `transformRequestToSaveToFilesystem`, `transform{Collection,Folder}RootToSave`, `transformCollectionToSaveToExportAsFile` | dropped before reaching disk / Bruno JSON export |\n| On-disk formats | `bruno-filestore/src/formats/{bru,yml}` (+ `bruno-lang`) — parse **and** stringify | lost in one format |\n| BRU → YML migration | `bruno-electron/src/ipc/yml-migration.js` (bru parse → yml stringify) | lost when a user migrates |\n| Collection config | `bruno.json` ↔ `opencollection.yml` — `utils/transformBrunoConfig.js`, `stringifyCollection`, migration's config transform (e.g. `transformProxyConfig`), CLI `getCollectionConfig` | config lost or misread by CLI |\n| OpenCollection import/export | `bruno-converters/src/opencollection/items/{http,graphql,grpc,websocket}.ts`, `folder.ts`, `environment.ts` — **both** directions; app entry `bruno-app/src/utils/exporters/opencollection.js` | lost on export or import |\n| Other converters | `postman/{postman-to-bruno,bruno-to-postman}.js`, `insomnia/`, `openapi/`, `wsdl/`; app exporters `postman-collection.js`, `openapi-spec.js`, `bruno-environment.js` | lost on import/export where the target can represent it |\n\nThe migration also rewrites content, not just shape: `bru.runRequest(\"x.bru\")` paths are stripped\nof `.bru`, and tab snapshots are remapped. A new script API that takes a file path, a new file\ntype, or a new `bruno.json` key needs a matching migration step.\n\n## 3. Scripting API ↔ QuickJS shim\n\n`bruno-js/src/{bru,bruno-request,bruno-response}.js` implement the API. The **QuickJS sandbox — the\ndefault \"safe\" mode — bridges each method explicitly** in `bruno-js/src/sandbox/quickjs/shims/`\n(`bru.js`, `bruno-request.js`, `bruno-response.js`, `bruno-grpc.js`, …). Implementation only →\nworks in developer mode, `undefined` for most users.\n\n## 4. Scope levels — collection ↔ folder ↔ request\n\nAuth, headers, vars, scripts and tests exist at three levels plus `inherit`. Each level has its own\nUI (`CollectionSettings/`, `FolderSettings/`, `RequestPane/`), and resolution lives in the merge\nhelpers in **each** runtime's `utils/collection.js`. A feature at one level needs a stated decision\nfor the other two and for `inherit`.\n\n## 5. Protocols — HTTP ↔ GraphQL ↔ gRPC ↔ WebSocket\n\n`REQUEST_TYPES` (`bruno-app/src/utils/common/constants.js`, `bruno-electron/src/utils/constants.js`)\nlists all four. Each has its own `parse*`/`stringify*` in `bruno-filestore/src/formats/yml/items/`\nand its own converter in `bruno-converters/src/opencollection/items/`. \"HTTP only\" is a legitimate\nscope — leaving it unstated is how a gap becomes a bug report. Widening CLI protocol support is a\ndeliberate decision, not an incidental one.\n\n## 6. Request shaping ↔ code generation\n\n`GenerateCodeItem/utils/snippet-generator.js` consumes headers, body, `settings` and resolved auth.\nA new auth mode or a setting that changes the wire request should change the generated snippet.\n\n## Parity checklist\n\n- [ ] Every twin in §1 opened; updated, or listed to the user with a reason\n- [ ] App-reported bug fixes verified on the CLI path\n- [ ] Shared logic moved to a leaf package where the DAG allows, instead of patched twice\n- [ ] Persisted field carried through every layer in §2 — including migration and converters (both directions)\n- [ ] New/changed `bru.*`, `req.*`, `res.*` API bridged in the QuickJS shim\n- [ ] Collection / folder / request levels and `inherit` each decided\n- [ ] gRPC and WebSocket considered; any \"HTTP only\" scope stated\n- [ ] Code generation updated if the wire request changed\n- [ ] Tests on **both** sides of every pair touched\n\nHuman-facing version of this map (for QA): Confluence *Related Feature Map — Change Impact &\nParity Checklist*, https://usebruno.atlassian.net/wiki/spaces/Bruno/pages/1376944136. Keep the two\nin step: add new pairings to both, and remove unified ones from both.\n\n<!-- File: .claude/rules/redux-store.md (Tokens: ~1832 | Category: anthropic-skill) -->\n---\npaths:\n  - \"packages/bruno-app/**/*\"\n---\n\n# React / Redux Architecture\n\n## Store configuration\n\n`configureStore` (Redux Toolkit **v1.8**, not v2) is set up in\n`src/providers/ReduxStore/index.js`. That file's `reducer` map is the **authoritative slice list**\n(~11 keys); read it rather than trusting any list here. Middleware is\n`getDefaultMiddleware().concat([...])` with **no options passed**, so RTK's defaults —\n`thunk`, plus `serializableCheck` and `immutableCheck` in dev — are all active.\n\n**Consequences to respect:**\n- **State must stay serializable.** Don't put functions, class instances, Promises, or DOM nodes\n  in the store — dev `serializableCheck` will flag them. Do **not** disable `serializableCheck` in\n  the real store to work around it (the only place it's disabled is a middleware unit test).\n- **Reducers mutate the Immer draft directly.** `createSlice` reducers write\n  `state.collections.push(...)`, `item.name = ...`, `.splice(...)` — do **not** spread-clone or\n  return new objects unless replacing wholesale. Match the surrounding reducer.\n\n## Slice organization\n\nSlices live in `src/providers/ReduxStore/slices/`, one domain each; `collections/` is by far the\nlargest (collection tree, requests, environments, runner, drafts — ~200 reducer action creators in\n`collections/index.js`, plus ~150 thunks in `collections/actions.js`, and more spread in from\n`collections/exampleReducers.js`). Read the specific slice for its real state and actions.\n\nTwo gotchas:\n- **Reducer key ≠ folder/file name.** e.g. `globalEnvironments` comes from\n  `slices/global-environments.js`; `openapiSync` from `slices/openapi-sync.js`.\n- **Action-type prefix follows the slice's `name:`, not the reducer key.** e.g.\n  `global-environments/…`, `collections/…`, `app/…`. Grep the action type, not the key, when\n  tracing dispatches.\n\n## Actions / thunks convention\n\nAsync logic is written as plain hand-written thunks — a function that returns\n`(dispatch, getState) => …`. `createAsyncThunk` isn't used anywhere today:\n\n```javascript\nexport const saveRequest = (itemUid, collectionUid) => (dispatch, getState) => { ... };\n// or async\nexport const openCollectionEvent = (uid, pathname, ...) => async (dispatch, getState) => { ... };\n```\n\nThunks live in the slice's `actions.js` (`slices/collections/actions.js` is the big one:\n`sendRequest`, `saveRequest`, `importCollection`, `pasteItem`, `deleteItem`, `openCollectionEvent`\n— the last handles the `main:collection-opened` IPC). Reducers (in the slice's `index.js`) stay\nsynchronous. Follow the plain-thunk shape by default so new code matches the rest — but this is a\nconvention, not a blocker: if a feature genuinely benefits from `createAsyncThunk` (e.g. its\nbuilt-in pending/fulfilled/rejected states), it's fine to reach for it deliberately.\n\n## Side-effect boundaries\n\n- **Reducers are pure** — no IPC, no network, no `Date.now`/random, no async. Immer draft\n  mutations only.\n- **Side effects live in thunks and middleware.** IPC is invoked from thunks via\n  `window.ipcRenderer` (e.g. `saveRequest` → `ipcRenderer.invoke('renderer:save-request', …)`); the\n  `collections/actions.js` file alone has ~200 `ipcRenderer` calls.\n- **The file-IO → open-tab pattern** is split across a thunk and the `tasks` middleware: a thunk\n  queues an `OPEN_REQUEST`/`OPEN_EXAMPLE` task (`insertTaskIntoQueue`) and fires the IPC write; the\n  `tasks` middleware watches for the resulting `main:*` file event, then dispatches `addTab` +\n  `removeTaskFromQueue`. Reuse this queue rather than opening the tab optimistically.\n\n## Custom middleware (`middlewares/`)\n\nFive middlewares, registered in `index.js` (read the directory + registration array for the live\nset):\n- **`tasks`** — listener middleware; opens a request/example tab after the file event for a queued\n  task arrives (see file-IO pattern above).\n- **`draft`** — on the first edit action, promotes a *preview* tab to permanent\n  (`handleMakeTabParmanent`). It does **not** write draft state — the reducers do.\n- **`autosave`** — debounced by `preferences.autoSave.interval`, gated on\n  `app.preferences.autoSave.enabled`; dispatches the `save*` thunks.\n- **`snapshot`** — debounced (~1s), gated on `app.snapshotReady`; persists a curated state subset\n  to disk (see Persistence).\n- **`debug`** (dev only) — logs every action; added only when `import.meta.env.MODE ===\n  'development'`.\n\n## Persistence / serialization constraints\n\nThe store isn't persisted wholesale, and there's no `redux-persist`. Instead the `snapshot`\nmiddleware serializes a **curated subset** — workspaces, collection metadata, per-collection tabs,\nselected environments, and devtools console open-state (`extras.devTools`, from the `logs` slice;\nsee `middlewares/snapshot/serializeSnapshot.js`) — and writes it to disk via IPC. Request/response\nbodies are deliberately not persisted. To persist a new piece of state, extend `serializeSnapshot`\nand its save-trigger list (`utils/snapshot`) — the snapshot is the established mechanism, so prefer\nit over adding `redux-persist` or `localStorage`.\n\n## Selectors\n\nInline `useSelector` in components is the norm (hundreds of usages); memoized `createSelector` is\nused in just one place (`src/selectors/tab.js`). Default to inline\n`useSelector`, and reach for `createSelector` when a derivation is genuinely expensive or reused.\n\n## What belongs in Redux (and what doesn't)\n\n- **In Redux:** draft / unsaved state (`item.draft`, `collection.draft`, `environmentsDraft`,\n  `globalEnvironmentDraft`), and cross-component or persisted UI state (`sidebarCollapsed`,\n  `leftSidebarWidth`, `taskQueue`, `item.collapsed`, active tab).\n- **Local `useState`:** ephemeral, component-scoped UI — search text, inline-create toggles, hover,\n  transient form state. Don't lift these into a slice.\n\n## Sidebar collection visibility\n\nCollections render in the sidebar only when they belong to the **active workspace**.\n`Sidebar/Collections/index.js` builds the list in a `useMemo` that walks\n`activeWorkspace.collections`, matches loaded collections through a `Map` keyed on\n`normalizePath(pathname)`, **excludes scratch collections** (`isScratchCollection`), adds \"ghost\"\ngit-remote rows (`GitRemoteCollectionRow`) for non-default workspaces, and sorts alphabetically.\nRead the current `useMemo` before changing this. Collapsed folders do **not** render their children\nin the DOM (conditional render on `item.collapsed`).\n\n## Sidebar DOM structure (for Playwright)\n\n`.collection-item-name` is the class on the **row-wrapper** div for every item (folders, requests,\nJS files) in `Sidebar/Collections/Collection/CollectionItem/index.js` — the wrapper also contains\nthe name span and `.menu-icon`. Use it for Playwright locators.\n\n## Providers (`src/providers/`)\n\nRead the directory for the current set: `App/` (IPC listeners in `useIpcEvents.js`, app init),\n`ReduxStore/`, `Theme/`, `Hotkeys/`, `Toaster/`, `PromptVariables/`.\n\n## Checks after changing a slice / middleware\n\nSlices and middleware have Jest coverage under `src/providers/ReduxStore/` (e.g.\n`slices/collections/*.spec.js`, `slices/global-environments.spec.js`,\n`middlewares/snapshot/serializeSnapshot.spec.js`). After a change, run the relevant spec:\n\n```bash\nnpm test --workspace=packages/bruno-app -- <path-or-pattern>\n```\n\nIf you touch the reducer map, thunks that fire IPC, or the snapshot subset, also sanity-check that\nstate stays serializable (no dev `serializableCheck` warnings) and that persisted shape still\nround-trips through `serializeSnapshot`.\n\n<!-- File: .claude/rules/sqlite.md (Tokens: ~2731 | Category: anthropic-skill) -->\n---\npaths:\n  - \"packages/bruno-sqlite/**/*\"\n  - \"packages/bruno-electron/src/ipc/sqlite.js\"\n  - \"packages/bruno-electron/src/services/sqlite/**/*\"\n---\n\n# SQLite SDK (`@usebruno/sqlite`)\n\nBruno's local database layer. You author **migrations** (`migrations/*.ts`) and **statements**\n(`statements/*.sql`); a codegen step compiles them into a typed data layer for the Electron main\nprocess, which owns the database and executes statements. The package has no renderer entry point:\nthe renderer reaches a statement only through an IPC handler written by hand for that statement in\n`bruno-electron`.\n\n**The renderer never sends SQL, and never picks the statement.** Each IPC channel is bound to exactly\none statement in the main process; the renderer sends only that statement's params. Do not add an\nIPC handler, statement, or option that accepts SQL text or a statement name from the renderer.\n\n## When to use it — and when not to\n\nUse it for state that is **derived, bulky, or secondary**: data Bruno can rebuild or lose without\ncorrupting anything the user authored, and that would bloat Redux or the IPC payloads if held in\nmemory.\n\nIt is **not** the right home for:\n\n- **Anything the user authored.** Collections, requests, environments, folders and `bruno.json` live\n  on disk as `.bru`/`.yml` — see `.claude/rules/dsl-changes.md`. Never mirror or migrate a\n  serialized field into SQLite.\n- **App preferences and small keyed state** — those are electron-store JSON files under\n  `src/store/` (see `.claude/rules/electron-ipc.md`).\n- **UI state** — Redux, or local `useState` (see `.claude/rules/redux-store.md`).\n\nThe database file is `bruno.db` under `app.getPath('userData')`, and the file store spills large\npayloads to `sqlite-files/` beside it. **Treat both as disposable**: the open path below deletes\nand rebuilds the database rather than failing (startup `collect()` then sweeps the spilled files no\nrow names), so never let either hold the only copy of anything.\n\n## Package layout\n\n```\npackages/bruno-sqlite/\n  migrations/<seq>_<name>.ts   authored — exports up()/down() returning SQL\n  statements/*.sql             authored — sqlc-style annotated statements\n  scripts/                     codegen (generate-artifacts), new-migration, verify-migrations,\n                               generate-schema\n  SCHEMA.md                    GENERATED, committed — ER diagram + table schemas\n  src/node/                    db.ts (migrations), statements.ts (prepare/execute)\n  src/shared/                  shared types\n  src/generated/               GENERATED, gitignored — never edit or commit\n  tests/node\n```\n\n**`@usebruno/sqlite` (= `/node`) is main-process only** (it imports `node:sqlite`, `node:fs`,\n`node:crypto`); never import it from the renderer. The package has zero internal `@usebruno/*`\ndependencies and must stay that way.\n\n`src/generated/` is produced from the authored migration and `.sql` files by `npm run generate`\n(which `prebuild`/`pretest` run for you). Those authored files are the only source of truth.\n\n## Adding a migration\n\n```bash\nnpm run migration:new --workspace=packages/bruno-sqlite    # prompts for a name\n```\n\nThis scaffolds `migrations/<7-digit sequence>_<name>.ts` exporting `up` and `down`, each returning\nthe SQL to apply or revert:\n\n```ts\nexport const up = (): string => {\n  return 'CREATE TABLE users (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, email TEXT);';\n}\nexport const down = (): string => {\n  return 'DROP TABLE users;';\n}\n```\n\nMigrations run at open time, in sequence order, each in its own transaction, and are recorded in a\n`_migrations` table with a sha256 of both `up` and `down`.\n\n- **A merged migration is immutable.** Editing one changes its hash; the next open throws\n  `does not match the migration already applied`, and `createDatabase` responds by **deleting the\n  user's database and rebuilding it**. Add a new migration instead — always.\n- **`down` must genuinely revert `up`.** It is not decoration: when the tail migration disappears\n  from the code (a user downgrading Bruno, or you switching branches), the recorded `down` is\n  executed automatically against the existing database. An empty or wrong `down` corrupts that path.\n- Only the **tail** rolls back (recorded sequences above the highest local one). Deleting a\n  migration from the middle silently leaves its schema in place — don't.\n- Design schema changes to tolerate an older Bruno opening the same file afterwards: prefer new\n  tables and nullable columns over reshaping an existing one.\n\nVerify against a real database rather than an empty one — this replays every migration on a\nthrowaway `VACUUM INTO` copy and then prepares every statement against the result:\n\n```bash\ncp .env.example .env    # set DB_PATH to an absolute path to a real bruno.db\nnpm run migration:verify --workspace=packages/bruno-sqlite\n```\n\n`SCHEMA.md` documents the resulting schema: a Mermaid ER diagram plus each table's columns, keys and\nindexes, produced by applying every migration to an empty in-memory database. The pre-commit hook\nregenerates and stages it whenever a file under `migrations/` is staged; run\n`npm run schema --workspace=packages/bruno-sqlite` to regenerate it by hand. Never edit it directly.\n\n## Adding statements\n\nStatements live in `statements/*.sql`, several per file, each introduced by a sqlc-style\nannotation. Params are **named** and always passed as an object — there is no positional binding:\n\n```sql\n-- name: list_users :many\nSELECT id, name, email FROM users ORDER BY name;\n\n-- name: get_user :one\nSELECT id, name, email FROM users WHERE id = @id;\n\n-- name: create_user :exec\nINSERT INTO users (name, email) VALUES (@name, @email);\n```\n\n| annotation | runs as | returns |\n|---|---|---|\n| `:one` | single row | row or `undefined` |\n| `:many` | rows | array |\n| `:exec` | write | `{ changes, lastInsertRowid }` |\n\n- Statement names are **globally unique across all `.sql` files** (a duplicate fails codegen).\n  Renaming a statement, or moving it to another file, means renaming its IPC channel and every\n  renderer call site too.\n- Use `@param` for consistency with the existing statements, even though `node:sqlite` also accepts\n  `:name` and `$name`.\n\n## Main process\n\n`packages/bruno-electron/src/services/sqlite/index.js` owns the single database instance:\n`openDatabase()` calls `createDatabase` on `bruno.db` and starts a background `files.collect()`; the\nservice exposes `getStatements()`, `getFiles()` (the SDK's file store — see the package README),\n`transaction(callback)`, `reclaimDiskSpace()` and `shutdown()`. It never hands out the `DB` itself,\nand it knows nothing about IPC. `index.js`'s ready block opens it (`sqliteService.openDatabase()`),\nand `before-quit` reclaims disk space and shuts it down.\n\n```js\nconst { getStatements, transaction } = require('../../services/sqlite');\n\ntry {\n  transaction(() => {\n    getStatements().execute('create_user', { name, email });\n  });\n} catch (err) {\n  // degrade: the database may be unavailable\n}\n```\n\n`getStatements()` never returns `null`. Before `openDatabase()`, after shutdown, and when\n`createDatabase` returns `{ db: undefined, statements: undefined }` (even the in-memory fallback\nfailed), it returns a stub whose `execute` logs the skipped statement and throws; `getFiles()`\nreturns a matching stub whose methods log and throw (or reject, for the async ones), and\n`transaction` throws the same way. So there is one failure mode, and every caller must handle it:\n\n- **`execute` and `transaction` throw** when the database is unavailable, for an unknown statement,\n  for one that couldn't be prepared against this schema (prepare failures are logged at construction\n  and only that statement is discarded — the rest of the database stays usable), and for any SQL\n  error. Wrap calls in try/catch and stay useful without the database — degrade, don't crash.\n\n`createDatabase` degrades rather than failing: file → (migration error) delete the `bruno.db*` files\nand rebuild → in-memory → `undefined`. A real backup is still a `TODO` in `src/node/index.ts`; it\ndeletes today.\n\n**`node:sqlite` is synchronous — every `execute` blocks the main process.** Keep statements indexed,\nbounded, and off hot paths; no full-table scans, no unbounded result sets, no per-event writes in a\ntight loop.\n\n## Exposing a statement to the renderer\n\nOnly statements the renderer actually needs are exposed, each with its own hand-written\n`ipcMain.handle` in `packages/bruno-electron/src/ipc/sqlite.js`. `registerSqliteIpc()` is called in\n`index.js` right after `openDatabase()`; each handler calls the service's `getStatements()` per\ninvocation, so a call after shutdown hits the stub, not a closed database.\n\nThe channel name follows a fixed convention: **`datastore:<statement_file>:<statement_name>`**, where\n`<statement_file>` is the `.sql` file's name without the extension, written exactly as the file is\nnamed. Channel names are written by hand, not generated.\n\n```js\nipcMain.handle('datastore:runner_responses:get_runner_response', (_event, params) => {\n  const request_uid = requireUid(params?.request_uid, 'request_uid');\n  return getStatements().execute('get_runner_response', { request_uid });\n});\n```\n\n- The handler validates and picks the params it forwards — never pass the renderer's object\n  straight through to `execute`.\n- The file store has no channels of its own. File ids are sequential and unscoped, so a channel\n  that takes one would expose every stored blob; the renderer reaches files only through a\n  domain handler that resolves them from a scoped key (e.g. `get_runner_response` by\n  `request_uid`). File writes stay main-process only — the renderer never chooses what gets\n  stored.\n- In the renderer, wrap each channel in a named hook or helper under `src/hooks/useX/index.js`\n  (or the relevant util) and call `window.ipcRenderer.invoke(channel, params)` there; no component\n  invokes a `datastore:*` channel directly.\n\n## Commands\n\n```bash\nnpm run build:bruno-sqlite                          # generate + rollup (from the repo root)\nnpm run watch --workspace=packages/bruno-sqlite     # rebuild on change\nnpm run test  --workspace=packages/bruno-sqlite     # jest; sets --experimental-sqlite for you\n```\n\n`npm run dev` does **not** rebuild this package, and bruno-electron consumes its `dist/` — after\nediting anything under `packages/bruno-sqlite`, rebuild or run the watcher or the app keeps the old\nstatements. Run the tests through the npm script: bare `jest` misses the\n`NODE_OPTIONS=\"--experimental-sqlite\"` the repo's Node needs for `node:sqlite`.\n\n## Before you call it done\n\n- [ ] New persistence genuinely belongs here — not `.bru`/`.yml`, electron-store, or Redux\n- [ ] No merged migration edited; new schema added as a new migration with a working `down`\n- [ ] `npm run generate` output shows the expected statements\n- [ ] `migration:verify` run against a real `bruno.db` copy\n- [ ] Main-process callers try/catch `execute` / `transaction`, and still work when the database\n      is unavailable\n- [ ] Each statement the renderer needs has its own `ipcMain.handle` on\n      `datastore:<statement_file>:<statement_name>`; renderer access goes through a named hook\n- [ ] Package rebuilt (`npm run build:bruno-sqlite`) before testing in the app\n- [ ] `npm run test --workspace=packages/bruno-sqlite` plus the affected app/electron specs\n\n<!-- File: .claude/rules/testing.md (Tokens: ~1561 | Category: anthropic-skill) -->\n---\npaths:\n  - \"tests/**/*\"\n  - \"playwright/**/*\"\n  - \"playwright.config.ts\"\n---\n\n# Playwright E2E Testing Guide\n\n> **Reference:** For the full narrative guide — test structure, the page-module locator/action pattern, and worked examples — see [`docs/playwright-testing-guide.md`](../../docs/playwright-testing-guide.md). This file is the terse, path-scoped quick reference; the guide is the canonical source for conventions.\n\n## Running Tests\n\n```bash\nnpm run test:e2e                           # default + system-pac + mock-server (starts dev servers automatically)\nnpx playwright test tests/request/         # Run specific directory\nnpx playwright test --project=default      # Run specific project\nnpx playwright test --headed               # Watch mode\n```\n\nProjects: `default` (main), `system-pac` (depends on `default`), `mock-server`, `auth`, `ssl`.\n`npm run test:e2e` runs `default` + `system-pac` + `mock-server`; `auth` and `ssl` have their own\n`test:e2e:*` scripts and CI jobs. Config: `playwright.config.ts` — `fullyParallel: true`, `workers` unset\n(Playwright default, not single-worker), retries 0 local / 2 CI.\n\n## Test Fixtures (playwright/index.ts)\n\nSee `playwright/index.ts` for the current set; the table covers the common ones.\n\n| Fixture | Scope | Purpose |\n|---------|-------|---------|\n| `page` | test | Default page from `electronApp` |\n| `electronApp` | worker | Default Electron launch (skips onboarding) |\n| `launchElectronApp(opts)` | worker | Custom launch with userData, env, init data |\n| `reuseOrLaunchElectronApp(opts)` | worker | Caches app instances by key |\n| `pageWithUserData` | test | Loads `init-user-data/` from test dir, waits for app ready |\n| `createTmpDir(tag?)` | worker | Temp directory, auto-cleaned after worker |\n| `collectionFixturePath` | test | Copies `fixtures/collection(s)/` to temp dir |\n| `restartApp(opts)` | test | Restart with fresh user data |\n| `newPage` | test | Fresh app instance with tracing |\n| `installFakeClipboard(page)` | test | Replaces `navigator.clipboard` on that page for the test; `copiedText()` returns what the app copied |\n\n## Test Data Conventions\n\n- `tests/<suite>/init-user-data/` — Seed data for existing-user tests (`pageWithUserData`)\n- `tests/<suite>/init-user-data-fresh/` — Seed data for new-user tests (hasLaunchedBefore: false)\n- `tests/<suite>/fixtures/collection(s)/` — Collection fixtures auto-copied to temp dirs\n- `initUserDataPath` files support `{{projectRoot}}` template variable\n- No `initUserDataPath` → default prefs with `hasLaunchedBefore: true` (skip onboarding)\n- `dotEnv` overrides defaults (e.g., `DISABLE_SAMPLE_COLLECTION_IMPORT: 'false'`)\n- Adding/changing a key in the app's `preferences.json` defaults? Mirror it in the `defaultPreferences` mock in `playwright/index.ts` — see [the guide](../../docs/playwright-testing-guide.md#3-keep-defaultpreferences-in-sync-with-app-preferences).\n\n## Test Helpers (tests/utils/)\n\nReuse existing helpers instead of hand-rolling clicks; read the module for its current exports\n(e.g. `grep \"^export\" tests/utils/page/actions.ts`). Modules by responsibility:\n\n- `page/actions.ts` — high-level user actions (create/open collections, requests, folders, environments; send/save requests; workspace + tab management; import)\n- `page/locators.ts` — locator builders (`buildCommonLocators(page)` + per-protocol/feature variants) and table helpers\n- `page/runner.ts` — collection/folder runner actions and result assertions\n- `tests/utils/cli.ts` — `runCLI(cwd, args)` for bruno-cli testing\n- `tests/utils/wait.ts` — polling helpers (e.g. `waitForPredicate(predicate, {tries, interval})`)\n\n**Convention:** a spec must never inline raw selectors. Each `page/*` module owns one UI section and exports both a locator builder and its actions; a new section gets a new file whose builder is mapped into `buildCommonLocators`. Full pattern + examples in [the guide](../../docs/playwright-testing-guide.md#1-centralize-locators-and-actions-in-page-modules).\n\n## Waiting for App Ready\n\n```javascript\nawait page.locator('[data-app-state=\"loaded\"]').waitFor();\n```\n\nThis attribute is set when `main:app-loaded` IPC arrives (independent of `renderer:ready` chain).\n\n## Mutating Fixtures\n\nThe suite must be isolated (unique tmp paths, no shared state).\n\nMost specs load fixtures via `collectionFixturePath` (copies `fixtures/collection(s)/` to a temp dir) or `pageWithUserData` (loads `init-user-data/` into a fresh temp userData dir). These operate on **temp copies** — they need **no** cleanup, even though they mutate files on disk (e.g. persistence specs writing to `environments/*.bru` or `collection.bru`). Only a spec that mutates a **committed** fixture **in place** must restore it in `afterAll` — those use `git checkout <fixturePath>`. Don't flag a missing `afterAll` on a temp-copy spec.\n\n## Common Pitfalls\n\n1. **Worker-scoped fixtures persist across test retries** — app state from failed attempts carries over. If test creates resources (folders, collections), retries may fail trying to create duplicates.\n\n2. **Collapsed folders hide children from DOM** — `toHaveCount`/`toBeVisible` won't find items inside collapsed folders. Expand folders before asserting on their children.\n\n3. **Sequential tests sharing state** — Tests in the same `describe` that depend on prior test state will cascade-fail. The `afterAll` cleanup only runs once, not between retries.\n\n4. **File watcher timing** — After creating files via IPC, the collection watcher must detect and process the change before the UI updates. Use Playwright's auto-retrying assertions (e.g., `expect(locator).toBeVisible()`) rather than immediate checks.\n\n5. **Default env vars in fixture** — `launchElectronApp` sets `DISABLE_SAMPLE_COLLECTION_IMPORT: 'true'` by default. Tests that need onboarding must override via `dotEnv`.\n\n6. **The renderer outlives the test** — `electronApp` is worker-scoped and `page` hands back the same window, so anything a spec patches into the page (a global, a stubbed browser API) leaks into later specs in that worker. Use a fixture that restores on teardown; the real clipboard is also shared across workers, so assert copies through `installFakeClipboard(page)`, not `navigator.clipboard.readText()`.\n\n## Test Coverage Areas\n\nTest suites live under `tests/`, one directory per feature area — list that directory for the\ncurrent set. Examples: `request/`, `collection/`, `environments/`, `runner/`,\n`scripting/`, `graphql/`, `grpc/`, `websockets/`, `auth/`, `ssl/`.\n\n<!-- File: .claude/skills/code-review/reviewers/_contract.md (Tokens: ~220 | Category: anthropic-skill) -->\n# Shared reviewer persona & output contract\n\nRead by every `code-review` reviewer. (Orchestration lives in `../SKILL.md`; the per-lens\nchecklists live in the sibling `*.md` files.)\n\n**Persona** — an expert reviewer in TypeScript, JavaScript, Node.js, and Electron on an\nenterprise team. Be **concise**: one clear sentence per finding; elaborate only when asked.\nReview to the project's standard regardless of who authored or requested the change — never\nsoften severity for assumed intent or seniority. Ground every finding in the actual code: trust\nthe repo over any doc, guide, or comment when they disagree, and never cite a line or invent an\nexample value you haven't verified in source.\n\n**Output contract** — return a flat list, one finding per line:\n\n```\n<blocker|suggestion|nit> | <file>:<line> | <one-sentence finding>\n```\n\nReturn `no findings` (nothing else) when the scope is clean. Never invent nits to fill the list.\n\n<!-- File: .claude/skills/code-review/reviewers/architecture.md (Tokens: ~383 | Category: anthropic-skill) -->\n# Architecture & dependency-boundaries reviewer\n\n**Scope:** `packages/**`.\n\nAdopt the reviewer persona and return findings in the output contract defined in\n`_contract.md`.\n\nReview the diff against **`.claude/rules/architecture.md`** — read its \"Dependency direction &\nownership boundaries\" section, plus \"Declared dependencies must match real imports\" when the diff\ntouches a `package.json`. The `@usebruno/*` packages form a strict dependency DAG and some carry hard\nplatform constraints. Report violations with `file:line`, severity:\n\n- **blocker** — a new internal `@usebruno/*` dependency that points *upward* or forms a cycle (a\n  shared lib importing `@usebruno/app` or `@usebruno/electron`); `bruno-common` importing a Node\n  built-in (`fs`/`path`/`os`/`crypto`/`child_process`/`node:*`) or any other `@usebruno/*` package\n  (it must stay browser-safe and dependency-free); `bruno-js` importing `electron`/IPC (it also runs\n  in the CLI); a runtime import of `bruno-schema-types` (types-only).\n- **suggestion** — code placed in the wrong package/layer: renderer-only logic in a shared lib,\n  host/Electron wiring pushed into `bruno-js`, or a util added to `bruno-common` that needs another\n  package; a new shared dependency that would be better kept local.\n- **suggestion** — manifest drift breaking the rule's **Declared dependencies must match real\n  imports**, read against the importing package's own `package.json`.\n- **Not a finding:** dependency edges already in the DAG, or a new *downward* dependency that fits\n  the established direction.\n\n<!-- File: .claude/skills/code-review/reviewers/conventions.md (Tokens: ~308 | Category: anthropic-skill) -->\n# Coding standards & readability reviewer\n\n**Scope:** all changed files (`**/*`).\n\nAdopt the reviewer persona and return findings in the output contract defined in\n`_contract.md`.\n\nReview the diff against **`.claude/rules/conventions.md`** (read it; it points to\n`CODING_STANDARDS.md`, the code-guidelines source of truth). Report each violation with\n`file:line`, severity:\n\n- **suggestion** — readability problems from the rule: unclear or misleading names, unnecessary\n  abstraction (indirection that earns no readability or reuse), single-line indirection, `?.` where\n  the null case isn't handled right there, needless whitespace/diff churn, or missing comments on\n  genuinely complex flow.\n- **suggestion** — any breach of the rule's **Reuse before you write** or **Replacing code leaves\n  nothing behind** sections; confirm what the new code actually renders or reads before calling a\n  leftover dead. From **Before you call it done**, report only what the diff itself shows: a new\n  branch or acceptance criterion with no test, or a changed return/payload shape whose assertions\n  elsewhere weren't updated. Whether the author ran the suite isn't visible here — don't guess at it.\n- **nit** — pure style/formatting/naming deviations (indent, quotes, semicolons, trailing commas,\n  arrow parens, casing) — most are auto-fixed by ESLint, so keep these brief.\n\n<!-- File: .claude/skills/code-review/reviewers/correctness.md (Tokens: ~632 | Category: anthropic-skill) -->\n# Correctness & root-cause reviewer\n\n**Scope:** all changed source (`**/*`, excluding `tests/**`).\n\nAdopt the reviewer persona and return findings in the output contract defined in\n`_contract.md`.\n\nValidate that the change is correct and, for every bug fix, that it addresses the\nunderlying problem rather than the visible symptom. Surface-level patches that mask a\ndefect without resolving it are a **blocker**:\n\n- Understand *why* the issue exists before accepting the fix — trace the bug to its origin,\n  don't stop at where it surfaced.\n- Flag patches that suppress a symptom (extra null guards, try/catch swallowing, defensive\n  re-checks, retries, timeouts, clamping values) while leaving the real cause in place. Ask:\n  if this defends against a bad value, *where does the bad value come from*, and should it be\n  fixed there instead?\n- A fix at the wrong layer (UI guard for a data-layer bug, caller working around a callee's\n  contract violation) is a symptom patch — call out the correct layer.\n- Watch for fixes scoped to one reproduction case when the same root cause can manifest\n  elsewhere; the correct fix usually covers all call sites, not just the reported one.\n- When a fix looks like a workaround, say so and name the deeper change that would actually\n  resolve it — even if the larger fix is out of scope, the PR should acknowledge it.\n- Beyond bug fixes, check ordinary correctness: off-by-one and boundary errors, unhandled\n  promise rejections / missing `await`, swallowed errors, incorrect null/undefined handling,\n  and edge cases the change introduces.\n- **Check the twin path.** Bruno keeps parallel implementations of the same behavior — `.bru`\n  vs `.yml` serializers, the default app-data workspace vs custom-filesystem workspaces, the\n  Electron request pipeline vs its `bruno-cli` copy, the scripting API vs its QuickJS shim,\n  `bruno-filestore` vs the `bruno-converters` export. A change that touches one path must be\n  verified against its twin; behavior that diverges between them (a value that persists or\n  defaults in one but is dropped or defaulted differently in the other) is a bug, not two\n  independent features. The full map of which surfaces pair up is\n  `.claude/rules/feature-parity.md`; the `feature-parity` lens reviews it in depth, so raise\n  only the correctness consequence here and leave the enumeration to that lens.\n- **`x || default` on a field whose absence is meaningful.** When \"not set\" / \"never\n  configured\" is a distinct state, falsy-coalescing (`version || '1'`) fabricates a value for\n  the unset case, erases the distinction, and often diverges from a sibling path that handles\n  it correctly. Flag it; use `??` or an explicit `undefined` check when the unset state must\n  survive.\n\n<!-- File: .claude/skills/code-review/reviewers/cross-platform.md (Tokens: ~284 | Category: anthropic-skill) -->\n# Cross-platform reviewer\n\n**Scope:** all changed files (`**/*`).\n\nAdopt the reviewer persona and return findings in the output contract defined in\n`_contract.md`.\n\nReview the diff against **`.claude/rules/cross-platform.md`** (read it) — Bruno ships on macOS,\nWindows, and Linux. Report each violation with `file:line`, severity:\n\n- **blocker** — anything that breaks a platform: hardcoded `/` or `\\` separators instead of\n  `path.join`/`path.resolve`; hardcoded paths (`/home/`, `C:\\Users\\`, `~/`) instead of\n  `os.homedir()`/`app.getPath()`; platform-specific shell/`child_process` without a fallback\n  (`which` vs `where`, `spawn` needing `shell:true` on Windows); Unix-only signals; `/tmp`\n  instead of `os.tmpdir()`.\n- **suggestion** — portable-but-fragile patterns not yet a concrete break: case-sensitivity\n  assumptions, inconsistent CRLF/LF handling, `fs.chmod`/`fs.access` assuming Unix permission bits.\n- **suggestion** — multiline `.bru`/text-block parsing that splits on `\\n` instead of a\n  CRLF-aware regex (`/\\r\\n|\\r|\\n/`) — see the rule's Line Endings section.\n\n<!-- File: .claude/skills/code-review/reviewers/dsl-changes.md (Tokens: ~552 | Category: anthropic-skill) -->\n# On-disk DSL & serialization reviewer\n\n**Scope:** `packages/bruno-app/**`, `packages/bruno-electron/**`, `packages/bruno-cli/**`,\n`packages/bruno-lang/**`, `packages/bruno-filestore/**`, `packages/bruno-schema/**`,\n`packages/bruno-schema-types/**`, `packages/bruno-converters/**`.\nThe format packages own serialization, but DSL objects are assembled in `bruno-app` (Redux)\nand `bruno-electron` (IPC), and `bruno-cli` reads/writes these files directly — a shape change\nmade there won't be caught by the serialization layer, so they're all in scope.\n\nAdopt the reviewer persona and return findings in the output contract defined in\n`_contract.md`.\n\nReview the diff against **`.claude/rules/dsl-changes.md`** (read it) — Bruno persists\ncollections/requests/environments/config as `.bru` and `.yml` files read across app versions, so\nbackward compatibility is the priority. Report violations with `file:line`, severity:\n\n- **blocker** — a breaking field change (rename, remove, retype, repurpose, or changed\n  default/semantics); a new field that isn't optional with a safe default; format drift between\n  `bru` and `yml` (or between parse and stringify); a lossy round-trip (`stringify` drops\n  unknown fields, or `parse(stringify(x)) !== x`); a shape change with no read-time compat shim;\n  a cryptic, abbreviated, or inconsistent property name (names are permanent and user-facing).\n- **blocker** — new `.bru` *syntax* (escape form, block delimiter, annotation grammar) that older\n  parsers can't read; an escaping/delimiter change not proven by on-disk reparse\n  (`parse(read(serialize(x)))`) or that escapes a multi-char delimiter by splitting on the whole\n  delimiter. See the rule's escaping and forward-compat sections.\n- **suggestion** — missing round-trip test in both formats or missing old-format golden fixture;\n  a name/structure that won't scale (positional vs keyed, widening union).\n- Also push back on any **unnecessary** DSL change that could be solved in memory / UI without\n  altering the serialized shape.\n- **Not a finding:** the read-accepts-`{content}` / write-emits-`string` asymmetry on\n  `description` fields — string-only internally is an established convention (see rule), not a\n  lossy round-trip.\n\n<!-- File: .claude/skills/code-review/reviewers/e2e-tests.md (Tokens: ~314 | Category: anthropic-skill) -->\n# E2E tests reviewer\n\n**Scope:** files under `tests/**` (`path: 'tests/**/**.*'`).\n\nAdopt the reviewer persona and return findings in the output contract defined in\n`_contract.md`.\n\nReview the diff against **`.claude/rules/testing.md`** and the e2e checklist in\n**`.coderabbit.yaml`** (read both). Report violations with `file:line`, severity:\n\n- **blocker** — `test.only`; `page.pause()`.\n- **suggestion** — `page.waitForTimeout()` where an `expect()` locator assertion could wait\n  instead; inline raw selectors instead of `tests/utils/page/*` modules; a spec that mutates a\n  committed fixture without restoring it in `afterAll`; shared state / non-isolated tmp paths.\n- **suggestion** — a non-discriminating assertion: `toContain('description:')` when the fixture\n  already carries that substring on unrelated rows passes even if the change under test never\n  happened; assert the unique value actually written by this change.\n- **suggestion** — parallel `.bru` and `.yml` fixtures for the same scenario that disagree\n  field-for-field (e.g. a row `selected: false` in one, enabled in the other) — a fixture bug\n  even when the current assertion tolerates it.\n- **nit** — locators not stored in variables; a single broad assertion where several focused\n  ones fit; steps not wrapped in `test.step`.\n\n<!-- File: .claude/skills/code-review/reviewers/feature-parity.md (Tokens: ~611 | Category: anthropic-skill) -->\n# Co-related features & parity reviewer\n\n**Scope:** `packages/**`.\n\nAdopt the reviewer persona and return findings in the output contract defined in\n`_contract.md`.\n\nReview the diff against **`.claude/rules/feature-parity.md`** (read it). This lens asks one\nquestion of every changed file: **what else had to change with it, and did it?** For each changed\nfile, find it in the rule's tables, open the counterpart, and check whether the counterpart logic\nis in the diff. Report with `file:line`, severity:\n\n- **blocker** — logic changed on one side of an app ↔ CLI pair (§1) when the twin file holds the\n  counterpart logic and the diff leaves it untouched. Includes bug fixes in `bruno-electron` whose\n  root cause `bruno-cli` also has a copy of. Name the twin path and function.\n- **blocker** — a scripting API added or changed in `bruno-js/src/{bru,bruno-request,bruno-response}.js`\n  without the matching bridge in `bruno-js/src/sandbox/quickjs/shims/`.\n- **blocker** — a persisted field that skips a §2 layer: missing from `bruno-schema` (throws on\n  save), the app save transform, one on-disk format, the BRU → YML migration, or OpenCollection\n  import/export (either direction). Flag the missing surface only — the on-disk contract itself\n  (compat, escaping, round-trip) belongs to the `dsl-changes` lens; don't duplicate its findings.\n- **suggestion** — identical logic patched in both runtimes that has no Electron- or CLI-only\n  dependency and could move into `@usebruno/requests` / `@usebruno/common` (`utils/cookies.js` is\n  the precedent); otherwise, a missing comment naming the pairing at both sites.\n- **suggestion** — a new app runner option with no `bru run` flag; a feature at one scope level\n  with no decision for the others or `inherit`; a wire-request change without a\n  `snippet-generator.js` update; an HTTP-only change on a path gRPC/WebSocket also flow through;\n  a Postman/Insomnia/OpenAPI converter not updated for a field its format can represent.\n- **suggestion** — tests on one side of a pair only.\n\n**Not a finding:** a stated asymmetry, or one the rule lists as known-divergent (no preferences\nstore / Electron window / system-browser OAuth / Yup validation in the CLI; CLI runner handles\nonly `http-request` and `graphql-request`). A change to runtime-specific code in a paired file\n(e.g. an Electron-only helper in `utils/filesystem.js`) is not a gap. Never claim a missing twin\nwithout confirming the twin file exists and holds the counterpart logic.\n\n<!-- File: .claude/skills/code-review/reviewers/react.md (Tokens: ~598 | Category: anthropic-skill) -->\n# React — app files reviewer\n\n**Scope:** `packages/bruno-app/**`.\n\nAdopt the reviewer persona and return findings in the output contract defined in\n`_contract.md`.\n\nReview changed components against **`CODING_STANDARDS.md` §React** (read it); for changes that\ntouch the store, also consult **`.claude/rules/redux-store.md`**. Report violations with\n`file:line`, severity:\n\n- **blocker** — an `useEffect` that could be derived state, an event handler, or a custom hook;\n  a hardcoded hex/rgb/hsl/named color instead of the styled-components `theme` prop (breaks the other\n  12 themes — verify the token path exists on the theme object); namespaced hook import (`React.useX`);\n  a component that mixes controlled and uncontrolled state; a hook called after a conditional\n  early return.\n- **blocker** — anything breaking **`.claude/rules/bruno-app-layout.md`** (read it),\n  component-specific vs shared placement included; the drift noted at the end of that file is not a\n  finding.\n- **blocker** — a new theme token defined in `themes/light|dark/*.js` without the matching property\n  added to `themes/schema/oss.js`, or added to the schema but missing from some of the 13 theme files.\n  The schema is `additionalProperties: false` and `providers/Theme/index.js` validates against it at\n  runtime, so either gap invalidates that whole theme and silently falls the user back to the default\n  with an error toast.\n- **suggestion** — a missing memo that breaks a dependency array or re-renders a heavy child, or\n  a gratuitous memo wrapping a cheap primitive; Tailwind used for colors (layout only);\n  a testable element without `data-testid`; a theme-token swap resolving to a different value than\n  the literal it replaced, or one value left hardcoded while its neighbours were themed (resolve it\n  in `themes/` first); a `document`/`window` listener not gated on the state that makes it relevant,\n  even where an incidental detail makes it harmless today.\n- **suggestion** — a new `EditableTable` column that omits the `readOnly`/`editMode` gating its\n  sibling columns carry (`readOnly={column.readOnly}` at render): the field stays editable in\n  view / read-only mode even though the change handler discards the edit.\n- **suggestion** — optimistic success state (`copied`, `saved`, `done`) set unconditionally\n  rather than gated on the operation resolving — e.g. `copied = true` after an optional-chained\n  `navigator.clipboard?.writeText` that no-ops in an insecure context — showing a false\n  confirmation.\n\n<!-- File: .claude/skills/code-review/reviewers/security.md (Tokens: ~410 | Category: anthropic-skill) -->\n# Security & data safety reviewer\n\n**Scope:** all changed source (`**/*`, excluding `tests/**`).\n\nAdopt the reviewer persona and return findings in the output contract defined in\n`_contract.md`.\n\nBruno is an offline API client that handles user credentials, tokens, and runs\nuser-authored scripts in a sandbox. Review changes for these Bruno-specific risks:\n\n- **No secret leakage.** Auth tokens, passwords, API keys, OAuth2 secrets, and env-var\n  values must never be written to logs, console, error messages, or telemetry. A secret\n  logged in cleartext is a **blocker**. Watch for `console.log`/logger calls that dump a\n  whole request, header set, or config object containing credentials.\n- **Script sandbox integrity** (`bruno-js`, QuickJS / Node VM). Changes that widen the\n  sandbox surface — exposing new Node built-ins, `require`, filesystem, or `process` to\n  user scripts, or passing unsanitized script output back into privileged code — are a\n  potential sandbox escape; call them out.\n- **IPC input validation** (`bruno-electron` handlers, `preload.js`). Treat every argument\n  arriving from the renderer as untrusted: validate file paths (guard against traversal\n  outside the collection dir), types, and bounds before acting on them.\n- **Path traversal & arbitrary writes.** File reads/writes derived from user or request\n  data must be confined to the intended directory — no `../` escape, no writing outside the\n  collection root.\n- **Injection & unsafe eval.** Flag string-built shell commands, `eval`, or dynamic\n  `Function` on untrusted input, and unescaped interpolation into commands or queries.\n- **Dependency & network surface.** Note new runtime dependencies or outbound network calls\n  that don't fit Bruno's offline-first, privacy-first posture.\n\nKeep findings concrete — tie each to how the tainted value reaches the sink.\n\n<!-- File: .claude/skills/code-review/SKILL.md (Tokens: ~1439 | Category: anthropic-skill) -->\n---\nname: code-review\ndescription: Review a Bruno diff, PR, or branch via focused reviewers run in parallel —\n  correctness, security, DSL, React, cross-platform, tests. Mirrors the CodeRabbit /\n  CODING_STANDARDS review.\n---\n\n# Reviewing Bruno code\n\nThis skill mirrors the automated CodeRabbit review (`.coderabbit.yaml`) so you can run\nthe same review locally. Source-of-truth order: coding standards → `CODING_STANDARDS.md`;\narchitecture/behavior → `.claude/rules/*`. `.coderabbit.yaml` mirrors these for CI parity —\nread it for a path instruction not summarized here, but the rules win if they ever disagree.\n\nThe review is **split into focused lenses that run in parallel**, so a large diff is\ncovered faster and each reviewer stays sharply scoped to one concern. Each lens lives in\nits own file under `reviewers/`. You (the orchestrator) dispatch the reviewers, then\nmerge and report their findings.\n\n## How to review (orchestration)\n\n1. **Get the diff.** Review only what changed — never untouched code. Pick the mode:\n   - **Committed range** (default): `git diff main...HEAD` against the base branch\n     (`main` or `release/*`). Update the base first (`git fetch` and confirm the local\n     base is current) — a stale base inflates the diff with already-merged, unrelated\n     changes and wastes a full fan-out. Each reviewer re-runs this scoped to its own\n     globs; since the range is pinned to fixed commits they all see identical bytes, so\n     there's no need to snapshot.\n   - **Working tree / uncommitted changes** (when asked to review unstaged or staged\n     work): the tree can shift mid-review, so re-running `git diff` per reviewer risks\n     each seeing a different snapshot. Instead, capture the diff **once** to a scratch\n     file and hand every reviewer that path. `git diff HEAD` covers staged + unstaged\n     tracked changes; run `git add -N .` first (reversible with `git reset`) so any new\n     untracked files also show up:\n     ```bash\n     git add -N . && git diff HEAD > \"$SCRATCH/review.diff\"\n     ```\n     `$SCRATCH` is your environment's scratchpad dir. Reviewers read this frozen diff for\n     their globs plus the on-disk files for surrounding context — the working-tree files\n     already hold the uncommitted state.\n2. **Enumerate changed files** — `git diff --name-only main...HEAD` (committed range) or\n   `git diff --name-only HEAD` (working tree). Use this to skip any lens whose file scope\n   isn't touched (e.g. no `packages/bruno-app/**` change → skip `react.md`; no `tests/**`\n   change → skip `e2e-tests.md`). Never skip the lenses scoped to all files, and never skip\n   `feature-parity.md` for a small diff — a one-file change is exactly the shape it catches.\n3. **Fan out the reviewers in parallel.** In a *single message*, launch one `Agent`\n   (subagent_type `Explore` or `general-purpose`) per in-scope reviewer below. Give each\n   subagent this exact briefing:\n   - The diff source — the committed range (e.g. `main...HEAD`) or the snapshot file path\n     (`$SCRATCH/review.diff`) — and the file globs it owns (from the reviewer file's\n     \"Scope\" line). For a snapshot, tell the reviewer to read that file for its globs\n     rather than re-run `git diff`.\n   - \"Read `.claude/skills/code-review/reviewers/_contract.md` for the shared persona and\n     output contract, then read `.claude/skills/code-review/reviewers/<file>` — and any rule\n     or source file it points to (e.g. `CODING_STANDARDS.md`, `.claude/rules/*.md`), which\n     hold the detailed checklist. Apply **only** that lens to the changed files in your\n     scope, at the severities the reviewer specifies. Do not review outside your scope.\"\n   Reviewers are read-only and independent — they don't coordinate, and overlap between\n   lenses is fine (you dedupe at merge time).\n4. **Merge and report.** Collect every reviewer's findings, drop exact duplicates, and\n   when two lenses flag the same `file:line` keep the higher severity. Regroup **by\n   file**, each finding tagged by severity (blocker / suggestion / nit) with `file:line`.\n   If the review request carries a problem statement or acceptance criteria (e.g. passed\n   as args), reconcile its enumerated deliverables — docs, migration notes, a test per new\n   default/branch — against the diff and flag any that are absent. If nothing's wrong, say\n   so briefly — don't manufacture nits.\n\n## Reviewers\n\nEach file is a self-contained checklist for one lens:\n\n| Reviewer file | Lens | Scope |\n|---|---|---|\n| `reviewers/correctness.md` | Correctness & root-cause | all source (excl. `tests/**`) |\n| `reviewers/architecture.md` | Architecture & dependency boundaries | `packages/**` |\n| `reviewers/conventions.md` | Coding standards & readability | all files |\n| `reviewers/react.md` | React — app files | `packages/bruno-app/**` |\n| `reviewers/cross-platform.md` | Cross-platform (macOS/Windows/Linux) | all files |\n| `reviewers/security.md` | Security & data safety | all source (excl. `tests/**`) |\n| `reviewers/dsl-changes.md` | On-disk DSL & serialization (backward compat) | `bruno-app`, `bruno-electron`, `bruno-cli`, `bruno-lang`, `bruno-filestore`, `bruno-schema(-types)`, `bruno-converters` |\n| `reviewers/feature-parity.md` | Co-related features & parity (app↔CLI, shim, converter, level, protocol twins) | `packages/**` |\n| `reviewers/e2e-tests.md` | Playwright E2E tests | `tests/**` |\n\n## Shared reviewer persona & output contract\n\nDefined once in `reviewers/_contract.md` — the small file every reviewer reads (dispatched\nreviewers read it, not this orchestration file). Keep the persona and the\n`<blocker|suggestion|nit> | <file>:<line> | <finding>` output shape there, not duplicated here,\nso a change updates a single place.\n\n<!-- File: .claude/skills/write-e2e-test/SKILL.md (Tokens: ~1088 | Category: anthropic-skill) -->\n---\nname: write-e2e-test\ndescription: Write a Playwright E2E test for Bruno. Use when adding or modifying\n  tests under tests/, creating a new test suite, or reproducing a bug as an\n  e2e spec.\n---\n\n# Writing a Bruno E2E test\n\nRead `.claude/rules/testing.md` first for the full fixture and helper reference. See\n`docs/playwright-testing-guide.md` for the extended walkthrough (codegen, running,\ndebugging, troubleshooting) — but where it disagrees with this skill or the actual repo,\ntrust the repo: specs live in `tests/` (not `e2e-tests/`), the suite is `fullyParallel`,\nand projects are `default`, `system-pac`, `auth`, and `ssl`\n(verify in `playwright.config.ts`).\n\n## Start with codegen or by hand\n\n- **Codegen** (fastest for a new flow): `npm run test:codegen <name>` launches the app,\n  records your clicks, and writes `tests/<name>.spec.ts`. Treat the output as a rough\n  draft — move it into the right area folder, swap generated selectors for `data-testid`\n  or helpers, wrap steps in `test.step`, and delete dead recorded actions.\n- **By hand**: `import { test, expect } from '../../playwright'` and follow the steps below.\n\n## Steps\n\n1. **Place the spec** at `tests/<area>/<name>.spec.ts` — reuse an existing area dir under\n   `tests/` (list it for the current set; e.g. `request/`, `collection/`, `environments/`,\n   `runner/`, `scripting/`) or add one. Pick the project: `default` for most specs;\n   `auth` / `ssl` / `system-pac` only when the test needs those\n   servers or external dependencies.\n2. **Use fixtures** from `playwright/index.ts` (read it for the full set) instead of launching\n   the app yourself:\n   - `page` / `electronApp` — default app with onboarding skipped.\n   - `pageWithUserData` — seeds data from `tests/<area>/init-user-data/`\n     (use `init-user-data-fresh/` for new-user flows).\n   - `createTmpDir(tag?)` — throwaway dir for collections/files the test creates; cleaned\n     up automatically, so prefer it over hardcoded paths.\n   - `newPage`, `launchElectronApp`, `reuseOrLaunchElectronApp`, `restartApp` — for\n     multi-window or app-restart scenarios.\n   - `collectionFixturePath` / `workspaceFixturePath` — copy a committed fixture to a tmp\n     dir and get its path.\n3. **Use helpers** from `tests/utils/page/` instead of hand-rolling clicks — read the module\n   for the current set (e.g. `createCollection`, `sendRequest`, `runCollection`).\n4. **Structure each test** arrange → act → assert, and wrap the phases in `test.step` so\n   the HTML report reads clearly. Prefer several focused assertions over one broad check.\n5. **Wait for readiness**: `await page.locator('[data-app-state=\"loaded\"]').waitFor()`\n   before asserting on app state.\n6. **Locate** by `data-testid` (Bruno's dominant convention) or documented classes\n   (`.collection-item-name`); stable `getByRole` / `getByText` locators are fine too.\n   Avoid brittle CSS, `#id`, or index-based selectors. If no stable selector exists, add a\n   `data-testid` to the component rather than locating by text.\n7. **If the test mutates a committed fixture**, restore it in `afterAll`. Existing specs\n   do this with `git checkout <fixturePath>` (e.g. `tests/response-examples/*.spec.ts`) —\n   follow the pattern already used by neighbouring specs in that area.\n8. **Use auto-retrying assertions** (`expect(locator).toBeVisible()`); never assert\n   immediately after a file-watcher-triggered change (created files take a tick to appear).\n   Reserve `page.waitForTimeout()` for when no locator assertion can wait instead.\n9. **Run it**: `npx playwright test tests/<area>/<name>.spec.ts --project=default`\n   (the web + test servers start automatically). Debug with `--ui`, `--headed`, `--debug`,\n   or `--trace on` (then `npx playwright show-trace test-results/**/trace.zip`).\n\n## Checklist before done\n\n- [ ] Test is isolated — unique tmp paths (`createTmpDir`), no state shared with other tests\n- [ ] Passes on a clean second run (retry-safe; worker fixtures persist across retries)\n- [ ] Folders expanded before asserting on their children (collapsed = not in DOM)\n- [ ] Any mutated committed fixture is restored in `afterAll`\n- [ ] Any codegen-generated draft refactored to fixtures/helpers + stable selectors\n- [ ] Steps wrapped in `test.step`; no `test.only`, no `page.pause()`\n\n<!-- File: packages/bruno-app/storybook/README.md (Tokens: ~1665 | Category: plugin-manifest) -->\n# Storybook\n\nStorybook is our workbench and living documentation for the `bruno-app` UI — primarily the design-system primitives under `src/ui`. Use it to build a component in isolation, see every state at once, and check how it looks across Bruno's themes.\n\n## Running\n\nFrom the repo root:\n\n```bash\nnpm run storybook --workspace=packages/bruno-app        # dev server on http://localhost:6006\nnpm run build-storybook --workspace=packages/bruno-app  # static build -> storybook-static/\n```\n\nConfig lives in this folder: `main.js` (framework, addons, stories glob, webpack aliases) and `preview.jsx` (global decorators, the theme toolbar).\n\n## Where stories live\n\nStories are co-located next to the component they document and picked up by the glob in `main.js`:\n\n```text\nsrc/**/*.stories.@(js|jsx|mjs|ts|tsx)\n```\n\nSo `src/ui/Button/index.js` is documented by `src/ui/Button/Button.stories.jsx`. `main.js` mirrors the app's path aliases (`ui`, `components`, `themes`, `providers`, `utils`, …), so imports like `import Button from 'ui/Button'` work inside stories.\n\n## Writing a story\n\nStandard CSF. Give the component a `Components/<Name>` title and tag it `autodocs` so it gets a Docs page:\n\n```jsx\nimport Button from './index';\n\nexport default {\n  title: 'Components/Button',\n  component: Button,\n  parameters: { layout: 'centered' },\n  tags: ['autodocs'],\n  argTypes: {\n    variant: { control: 'select', options: ['filled', 'outline', 'ghost'] }\n  }\n};\n\nexport const Default = { args: { children: 'Button' } };\n```\n\nTheming is automatic — `preview.jsx` wraps every story in the styled-components `ThemeProvider` (and the app's `ThemeContext`), so components read `props.theme` and `useTheme()` normally. Don't add your own theme provider.\n\n## Theme switching\n\nThe toolbar has a **Theme** picker listing all of Bruno's registered themes (from `themes/index.js` `themeRegistry`). Selecting one re-renders the current story with that theme's real tokens and sets the preview's light/dark mode.\n\nNote: components styled via styled-components `props.theme` (nearly all of `src/ui`) get full per-theme accuracy. Anything relying on raw `var(--color-*)` CSS variables only toggles light vs dark, since those variables aren't per-theme.\n\n## Conventions & gotchas\n\nThese are worth following so components render correctly on the **Docs** page, where every story renders inline in one shared document (unlike Canvas, which isolates each story in an iframe).\n\n**Keep the Docs description clean.** Storybook uses a component's leading JSDoc as the Docs description and does not strip `@param` tags, so a rich JSDoc block dumps as a run-on paragraph. Set an explicit description in the story instead of trimming the component's source comments:\n\n```js\nparameters: {\n  docs: { description: { component: 'One clean paragraph. See the props table below.' } }\n}\n```\n\n**Don't hardcode `name` on radio-based groups.** `RadioGroup`/`SegmentedControl` auto-generate a unique `name` per instance. On the Docs page the primary story renders twice and all stories share the DOM, so a hardcoded `name` makes separate instances collide into one native radio group (selecting in one unchecks another). Omit `name` unless you specifically want a shared native group.\n\n**Give overlay components room.** Dropdowns, menus, and modals open a popover that gets clipped by the short, `overflow`-hidden inline Docs preview. Render those stories in an iframe with a height:\n\n```js\nparameters: { docs: { story: { inline: false, height: '360px' } } }\n```\n\n**Always give group components an accessible name.** Components like `RadioGroup`/`SegmentedControl` warn in development if they render without `label`, `ariaLabel`, or `ariaLabelledBy` — pass one in stories and real usage. Don't fabricate a placeholder name to silence the warning; a meaningless label is worse than none (only the shared native `name` attribute is auto-generated, because it carries no human meaning).\n\n**Inline story state — don't wrap in a helper component.** For controlled components, put `useState` directly inside the story's `render` and return the component itself. Wrapping it in a `<Controlled>` helper makes the code panel show `<Controlled>` instead of the real component, because the generated source reflects the root element the `render` returns.\n\n```jsx\nexport const Default = {\n  render: (args) => {\n    const [value, setValue] = useState('http');\n    return <SegmentedControl {...args} value={value} onChange={setValue} items={items} />;\n  }\n};\n```\n\n**Demo wrappers, not component chrome.** If a story needs a sized/bordered container to demonstrate behavior (e.g. width for overflow), keep it clearly a demo wrapper so it isn't mistaken for the component's own styling.\n\n## Docs addon\n\nAutodocs (the Docs tab) requires `@storybook/addon-docs`, registered in `main.js` and pinned to the **same version** as the `storybook` core package — a version mismatch throws `docsParameter.renderer is not a function`. Run `npx storybook doctor` to check for mismatched Storybook packages.\n\n## Code panel\n\n`preview.jsx` turns on the source/code views that `@storybook/addon-docs` provides:\n\n```js\ndocs: {\n  codePanel: true,              // a \"Code\" tab in the Canvas addon panel\n  canvas: { sourceState: 'shown' } // Docs \"Show code\" expanded by default\n}\n```\n\nThe snippet reflects the root element your story's `render` returns, so follow the \"inline story state\" note above to keep it showing the real component. There's also a `</>` Show code button in the Canvas toolbar.\n\n## Adding a new primitive\n\n1. Build the component under `src/ui/<Name>/` (`index.js` + `StyledWrapper.js`), following the existing conventions: `$`-prefixed transient style props, theme tokens, a `dataTestId`, forwarded refs, and forwarded `...rest`.\n2. Add `<Name>.stories.jsx` next to it with a `Components/<Name>` title, `autodocs`, `argTypes`, a clean `docs.description.component`, and a story per meaningful state.\n3. Add `<Name>.spec.jsx` (React Testing Library) for behavior and accessibility.\n\n### Group primitives (single-select)\n\nSingle-select group primitives (`RadioGroup`, `SegmentedControl`) follow a specific layout: the implementation lives in a `components/` subfolder and only the data-driven **list** API is exposed publicly, keeping the compound pieces internal so a compound API can be surfaced later without a rewrite.\n\n```text\nsrc/ui/<Name>/\n├── components/\n│   ├── <Name>Base.jsx    # compound: owns the context + renders the radiogroup\n│   ├── <Option>.jsx      # the internal option (reads context)\n│   ├── <Name>.jsx        # public list component — maps `items` → options\n│   └── StyledWrapper.js  # styling\n├── index.jsx             # exposes only the list <Name> (default export)\n├── <Name>.stories.jsx\n└── <Name>.spec.jsx\n```\n\nConsumers use the list API: `<SegmentedControl value onChange items={[{ value, label, ... }]} />`. To expose the compound API later, re-export `<Option>` / `<Name>Base` from `index.jsx`.\n\n<!-- File: packages/bruno-cli/docker/images/alpine/README.md (Tokens: ~161 | Category: plugin-manifest) -->\n# Bruno CLI — Alpine\n\nAlpine Linux variant of the Bruno CLI Docker image.\n\n**Base image:** `node:22-alpine`\n\n## Building\n\n```bash\ndocker build -t usebruno/cli:alpine ./images/alpine\n\n# with specific Bruno CLI version\ndocker build \\\n  --build-arg BRUNO_VERSION=3.3.0 \\\n  -t usebruno/cli:3.3.0-alpine \\\n  ./images/alpine\n```\n\n## Usage\n\n```bash\n# Run a collection\ndocker run -v $(pwd):/bruno usebruno/cli:alpine run\n\n# with pinned version\ndocker run -v $(pwd):/bruno usebruno/cli:3.3.0-alpine run\n```\n\n<!-- File: packages/bruno-cli/docker/images/debian/README.md (Tokens: ~159 | Category: plugin-manifest) -->\n# Bruno CLI — Debian\n\nDebian slim variant of the Bruno CLI Docker image.\n\n**Base image:** `node:22-slim`\n\n## Building\n\n```bash\ndocker build -t usebruno/cli:debian ./images/debian\n\n# with specific Bruno CLI version\ndocker build \\\n  --build-arg BRUNO_VERSION=3.3.0 \\\n  -t usebruno/cli:3.3.0-debian \\\n  ./images/debian\n```\n\n## Usage\n\n```bash\n# Run a collection\ndocker run -v $(pwd):/bruno usebruno/cli:debian run \n\n# with pinned version\ndocker run -v $(pwd):/bruno usebruno/cli:3.3.0-debian run \n```\n\n<!-- File: packages/bruno-cli/docker/README.md (Tokens: ~2159 | Category: plugin-manifest) -->\n# Bruno CLI Docker Images\n\nOfficial Docker images for [Bruno CLI](https://www.usebruno.com), enabling container-native API collection runs in CI/CD pipelines and local environments without requiring Node.js or npm on the host. See the [Bruno CLI docs](https://docs.usebruno.com/bru-cli/overview) for CLI usage.\n\n## Registries\n\n```bash\ndocker pull usebruno/cli:latest\ndocker pull ghcr.io/usebruno/cli:latest\n```\n\n---\n\n## Variants\n\n| Variant | Base image | Details |\n|---------|-----------|---------|\n| **Alpine** (default) | `node:22-alpine` | [→ Alpine README](https://github.com/usebruno/bruno/blob/main/packages/bruno-cli/docker/images/alpine/README.md) |\n| **Debian** | `node:22-slim` | [→ Debian README](https://github.com/usebruno/bruno/blob/main/packages/bruno-cli/docker/images/debian/README.md) |\n\n### Quick choice\n\n- **Use Alpine** unless you have a specific reason not to (90% of users)\n- **Use Debian** if you hit SSL/glibc compatibility issues\n\n---\n\n## Tags\n\nEvery release publishes the following tags to **both** Docker Hub (`usebruno/cli`) and GHCR (`ghcr.io/usebruno/cli`).\n\n### Alpine variant (default)\n\n| Tag pattern | Example | Notes |\n|-------------|---------|-------|\n| `latest` | `usebruno/cli:latest` | Newest release marked as latest. Only moves when the publish workflow is run with \"Tag this version as latest\" checked. |\n| `latest-alpine` | `usebruno/cli:latest-alpine` | Alias of `latest` — also alpine. |\n| `alpine` | `usebruno/cli:alpine` | Newest alpine, moves on every alpine publish. |\n| `<version>` | `usebruno/cli:3.3.0` | Exact version, immutable. |\n| `<version>-alpine` | `usebruno/cli:3.3.0-alpine` | Exact version, explicitly alpine. |\n| `<major.minor>` | `usebruno/cli:3.3` | Floats with patch releases (3.3.x). |\n| `<major.minor>-alpine` | `usebruno/cli:3.3-alpine` | Same, explicitly alpine. |\n| `<major>` | `usebruno/cli:3` | Floats with any 3.x.x release. |\n| `<major>-alpine` | `usebruno/cli:3-alpine` | Same, explicitly alpine. |\n\n### Debian variant\n\n| Tag pattern | Example | Notes |\n|-------------|---------|-------|\n| `latest-debian` | `usebruno/cli:latest-debian` | Newest debian release marked as latest (gated by the same checkbox). |\n| `debian` | `usebruno/cli:debian` | Newest debian, moves on every debian publish. |\n| `<version>-debian` | `usebruno/cli:3.3.0-debian` | Exact version, debian. |\n| `<major.minor>-debian` | `usebruno/cli:3.3-debian` | Floats with debian patch releases. |\n| `<major>-debian` | `usebruno/cli:3-debian` | Floats with any 3.x.x debian release. |\n\nThe unsuffixed tags (`:latest`, `:3.3.0`, `:3.3`, `:3`, `:alpine`) always resolve to the alpine variant by convention.\n\n---\n\n## Step-by-step guide\n\n### Step 1 — Pull the image\n\n```bash\n# latest (alpine by default — smallest, fastest to pull)\ndocker pull usebruno/cli:latest\n\n# specific version (recommended for production CI)\ndocker pull usebruno/cli:3.3.0\n\n# major.minor — gets patch updates automatically\ndocker pull usebruno/cli:3.3\n\n# debian variant\ndocker pull usebruno/cli:debian\ndocker pull usebruno/cli:3.3.0-debian\n```\n\n---\n\n#### Check it works\n\n```bash\ndocker run --rm usebruno/cli --version\n```\n\n---\n\n### Step 2 — Run your collection\n\n> These examples assume you are running `docker` from your Bruno collection directory. Mount that directory to `/bruno` and pass `bru` arguments directly after the image name. If your collection lives elsewhere on disk, see the path-based examples further down.\n\n> **Cross-platform note:** the examples below use `$(pwd)` which works in Bash / Zsh / Git Bash / WSL. On Windows native shells, substitute `$(pwd)` with `${PWD}` (PowerShell) or `%cd%` (CMD).\n\n> **Adding `bru` options:** The examples below show docker-specific flags. For all `bru run` options — recurse (`-r`), environments, variables, reporters, bail, and more — see the [Bruno CLI docs](https://docs.usebruno.com/bru-cli/overview).\n\n```bash\n# run every request in the Bruno collection (current dir)\ndocker run -v $(pwd):/bruno usebruno/cli run \n\n# run a specific subfolder (group of requests) within that collection\ndocker run -v $(pwd):/bruno usebruno/cli run ./api-tests\n\n# run a single .bru request file from that collection\ndocker run -v $(pwd):/bruno usebruno/cli run ./api-tests/login.bru\n\n# write a JUnit XML report (lands in the current directory because of the bind mount)\ndocker run -v $(pwd):/bruno usebruno/cli run --reporter-junit results.xml\n```\n\nFor Windows CMD users, swap `$(pwd)` with `%cd%`:\n\n```cmd\ndocker run -v %cd%:/bruno usebruno/cli run \n```\n\n#### Running a collection that lives at a different path\n\nIf your collection is not in your current directory, point `docker` at its path (relative or absolute) instead of `$(pwd)`:\n\n```bash\n# run every request in a collection at an arbitrary path\ndocker run -v /path/to/your/collection:/bruno usebruno/cli run\n\n# run a single .bru file from a collection at an arbitrary path\ndocker run -v /path/to/your/collection:/bruno usebruno/cli run ./auth/login.bru\n```\n\n> **Note on `--rm`:** Examples below include `--rm`. Docker keeps stopped containers around after they exit, which lets you `docker logs` or `docker inspect` them later for debugging. If you'd rather have Docker auto-delete the container as soon as `bru` finishes — useful for CI runs or to avoid `docker ps -a` filling up with stale entries — append `--rm` to any `docker run` (or `docker compose run`) command:\n\n```bash\ndocker run --rm -v $(pwd):/bruno usebruno/cli run\n```\n\n> It's purely a cleanup convenience; it doesn't affect the image, mounts, stdout output, or exit code.\n\n\n## CI/CD integration\n\n### GitHub Actions\n\n```yaml\njobs:\n  api-tests:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n\n      - name: Run Bruno collection\n        run: |\n          docker run --rm \\\n            -v ${{ github.workspace }}:/bruno \\\n            usebruno/cli:latest run --output results.xml --format junit\n\n      - name: Publish Test Report\n        uses: dorny/test-reporter@v3\n        if: success() || failure()\n        with:\n          name: Bruno Test Results\n          path: ${{github.workspace}}/results.xml\n          reporter: java-junit\n```\n\n### GitLab CI\n\n```yaml\napi-tests:\n  image: usebruno/cli:latest\n  script:\n    - bru run --output results.xml --format junit\n  artifacts:\n    reports:\n      junit: results.xml\n```\n\n---\n\n## Docker Compose\n\n### Quick example\n\nA minimal `docker-compose.yml` for running a Bruno collection alongside your project:\n\n```yaml\nservices:\n  bruno-cli:\n    image: usebruno/cli:latest\n    container_name: bruno-cli-runner\n    volumes:\n      - /path/to/collection:/bruno\n      - /path/to/reports:/reports\n    command:\n      run .\n      -r\n      --env ci\n      --reporter-json /reports/results.json\n      --reporter-junit /reports/results.xml\n      --reporter-html /reports/results.html\n```\n\nThen run:\n\n```bash\ndocker compose run bruno-cli\n```\n\nThe `/path/to/reports:/reports` mount catches the JSON, JUnit XML, and HTML reports on the host — drop any `--reporter-*` flag to skip that format.\n\n### Try it from this repo\n\nA ready-to-run `docker-compose.yml` lives in this repo at [`packages/bruno-tests/docker-compose.yml`](https://github.com/usebruno/bruno/blob/main/packages/bruno-tests/docker-compose.yml). It mounts the sibling `collection/` directory into the container, runs the `echo` folder against the `Prod` environment, and writes JSON, JUnit XML, and HTML reports into `packages/bruno-tests/reports/`:\n\n```bash\ncd packages/bruno-tests\ndocker compose run bruno-cli\n```\n\nThis fires a small set of requests against public endpoints that demonstrate the CLI executing requests and assertions inside a container.\n\n### Standalone demo\n\nFor a clone-and-run demo with a curated collection, see [`bruno-collections/bruno-cli-docker`](https://github.com/bruno-collections/bruno-cli-docker).\n\n---\n\n## Image details\n\nAll variants include:\n\n- **Entrypoint:** `bru`\n- **Working directory:** `/bruno`\n- **User:** `node` (UID 1000, non-root)\n- **Architectures:** `linux/amd64`, `linux/arm64`\n\n<!-- File: packages/bruno-cli/readme.md (Tokens: ~1280 | Category: plugin-manifest) -->\n# Bruno CLI\n\nWith Bruno CLI, you can now run your API collections with ease using simple command line commands.\n\nThis makes it easier to test your APIs in different environments, automate your testing process, and integrate your API tests with your continuous integration and deployment workflows.\n\nFor detailed documentation, visit [Bruno CLI Documentation](https://docs.usebruno.com/bru-cli/overview).\n\n## Installation\n\nTo install the Bruno CLI, use the node package manager of your choice, such as NPM:\n\n```bash\nnpm install -g @usebruno/cli\n```\n\n## Getting started\n\nNavigate to the directory where your API collection resides, and then run:\n\n```bash\nbru run\n```\n\nThis command will run all the requests in your collection. You can also run a single request by specifying its filename:\n\n```bash\nbru run request.bru\n```\n\nOr run all requests in a collection's subfolder:\n\n```bash\nbru run folder\n```\n\nIf you need to use an environment, you can specify it with the `--env` option:\n\n```bash\nbru run folder --env Local\n```\n\nIf you need to collect the results of your API tests, you can specify the `--output` option:\n\n```bash\nbru run folder --output results.json\n```\n\nIf you need to run a set of requests that connect to peers with both publicly and privately signed certificates respectively, you can add private CA certificates via the `--cacert` option. By default, these certificates will be used in addition to the default truststore:\n\n```bash\nbru run folder --cacert myCustomCA.pem\n```\n\nIf you need to limit the trusted CA to a specified set when validating the request peer, provide them via `--cacert` and in addition use `--ignore-truststore` to disable the default truststore:\n\n```bash\nbru run request.bru --cacert myCustomCA.pem --ignore-truststore\n```\n\n## Importing Collections\n\nYou can import collections from other formats, such as OpenAPI, using the import command:\n\n```bash\nbru import openapi --source api.yml --output ~/Desktop/my-collection --collection-name \"My API\"\n```\n\nYou can also use the shorter form with aliases:\n\n```bash\nbru import openapi -s api.yml -o ~/Desktop/my-collection -n \"My API\"\n```\n\nThis creates a Bruno collection directory that can be opened in Bruno.\n\nYou can also import directly from a URL:\n\n```bash\nbru import openapi --source https://example.com/api-spec.json --output ~/Desktop --collection-name \"Remote API\"\n```\n\nYou can also export the collection as a JSON file:\n\n```bash\nbru import openapi --source api.yml --output-file ~/Desktop/my-collection.json --collection-name \"My API\"\n```\n\nImport Options:\n\n| Option                    | Details                                            |\n| ------------------------- | -------------------------------------------------- |\n| --source, -s              | Path to the source file or URL (required)          |\n| --output, -o              | Path to the output directory                       |\n| --output-file, -f         | Path to the output JSON file                       |\n| --collection-name, -n     | Name for the imported collection                   |\n| --insecure                | Skip SSL certificate validation when fetching from URLs |\n\n## Command Line Options\n\n| Option                       | Details                                                                       |\n| ---------------------------- | ----------------------------------------------------------------------------- |\n| -h, --help                   | Show help                                                                     |\n| --version                    | Show version number                                                           |\n| -r                           | Indicates a recursive run (default: false)                                    |\n| --cacert [string]            | CA certificate to verify peer against                                         |\n| --env [string]               | Specify environment to run with                                               |\n| --env-var [string]           | Overwrite a single environment variable, multiple usages possible             |\n| -o, --output [string]        | Path to write file results to                                                 |\n| -f, --format [string]        | Format of the file results; available formats are \"json\" (default) or \"junit\" |\n| --reporter-json [string]     | Path to generate a JSON report                                                |\n| --reporter-junit [string]    | Path to generate a JUnit report                                               |\n| --reporter-html [string]     | Path to generate an HTML report                                               |\n| --insecure                   | Allow insecure server connections                                             |\n| --tests-only                 | Only run requests that have tests                                             |\n| --bail                       | Stop execution after a failure of a request, test, or assertion               |\n| --csv-file-path              | CSV file to run the collection with                                           |\n| --reporter--skip-all-headers | Skip all headers in the report                                                |\n| --reporter-skip-headers      | Skip specific headers in the report                                           |\n| --client-cert-config         | Client certificate configuration by passing a JSON file                       |\n| --delay [number]             | Add delay to each request                                                     |\n\n## Scripting\n\nBruno cli returns the following exit status codes:\n\n- `0` -- execution successful\n- `1` -- an assertion, test, or request in the executed collection failed\n- `2` -- the specified output directory does not exist\n- `3` -- the request chain seems to loop endlessly\n- `4` -- bru was called outside of a collection root directory\n- `5` -- the specified input file does not exist\n- `6` -- the specified environment does not exist\n- `7` -- the environment override was not a string or object\n- `8` -- an environment override is malformed\n- `9` -- an invalid output format was requested\n- `255` -- another error occurred\n\n## Demo\n\n![demo](assets/images/cli-demo.png)\n\n## Support\n\nIf you encounter any issues or have any feedback or suggestions, please raise them on our [GitHub repository](https://github.com/usebruno/bruno)\n\nThank you for using Bruno CLI!\n\n## Changelog\n\n<!-- An absolute link is used here because npm treats links differently -->\n\nSee [https://github.com/usebruno/bruno/releases](https://github.com/usebruno/bruno/releases)\n\n## License\n\n[MIT](license.md)\n\n<!-- File: packages/bruno-common/readme.md (Tokens: ~39 | Category: plugin-manifest) -->\n# bruno-common\n\nA collection of common utilities used across Bruno App, Electron and CLI packages.\n\n### Publish to Npm Registry\n\n```bash\nnpm publish --access=public\n```\n\n<!-- File: packages/bruno-converters/readme.md (Tokens: ~962 | Category: plugin-manifest) -->\n# bruno-converters\n\nThe converters package is responsible for converting collections from one format to a Bruno collection.\nIt can be used as a standalone package or as a part of the Bruno framework.\n\n## Installation\n\n```bash\nnpm install @usebruno/converters\n```\n\n## Usage\n\n### Convert Postman collection to Bruno collection\n\n```javascript\nconst { postmanToBruno } = require('@usebruno/converters');\n\n// Convert Postman collection to Bruno collection\nconst brunoCollection = postmanToBruno(postmanCollection);\n```\n\n### Convert Postman Environment to Bruno Environment\n\n```javascript\nconst { postmanToBrunoEnvironment } = require('@usebruno/converters');\n\nconst brunoEnvironment = postmanToBrunoEnvironment(postmanEnvironment);\n```\n\n### Convert Insomnia collection to Bruno collection\n\n```javascript\nconst { insomniaToBruno } = require('@usebruno/converters');\n\nconst brunoCollection = insomniaToBruno(insomniaCollection);\n```\n\n### Convert OpenAPI specification to Bruno collection\n\n```javascript\nconst { openApiToBruno } = require('@usebruno/converters');\n\nconst brunoCollection = openApiToBruno(openApiSpecification);\n```\n\n### Convert WSDL file to Bruno collection\n\n```javascript\nimport { wsdlToBruno } from '@usebruno/converters';\n\nconst brunoCollection = await wsdlToBruno(wsdlContent);\n```\n\n## Example \n\n```javascript\n\nconst { postmanToBruno } = require('@usebruno/converters');\nconst { writeCollectionToDisk } = require('@usebruno/filestore');\nconst fs = require('fs/promises');\n\nasync function convertPostmanToBruno(inputFile, outputDir) {\n  const inputData = await fs.readFile(inputFile, 'utf8');\n\n  const collection = await postmanToBruno(JSON.parse(inputData));\n  // Writes YAML (OpenCollection) files by default\n  await writeCollectionToDisk(collection, outputDir);\n  // Pass { format: 'bru' } to write .bru files instead\n  // await writeCollectionToDisk(collection, outputDir, { format: 'bru' });\n}\n\nconvertPostmanToBruno('demo_collection.postman_collection.json', './bruno-collection');\n\n``` \n\n## WSDL Import Features\n\nThe WSDL importer supports the following features:\n\n- **Service Discovery**: Automatically extracts service endpoints from WSDL definitions\n- **Operation Mapping**: Converts WSDL operations to Bruno HTTP requests\n- **SOAP Envelope Generation**: Creates proper SOAP envelopes for each operation\n- **Header Configuration**: Sets up appropriate Content-Type and SOAPAction headers\n- **Environment Variables**: Creates environment variables for service base URLs\n- **Folder Organization**: Groups operations by port type for better organization\n\n### WSDL Import Example\n\n```javascript\nimport { wsdlToBruno } from '@usebruno/converters';\nimport { writeCollectionToDisk } from '@usebruno/filestore';\nimport fs from 'fs/promises';\n\nasync function importWSDL() {\n  const wsdlContent = await fs.readFile('service.wsdl', 'utf8');\n\n  const collection = await wsdlToBruno(wsdlContent);\n  // Writes YAML (OpenCollection) files by default\n  await writeCollectionToDisk(collection, './soap-collection');\n  // Pass { format: 'bru' } to write .bru files instead\n  // await writeCollectionToDisk(collection, './soap-collection', { format: 'bru' });\n}\n\nimportWSDL();\n```\n\n### CLI Usage\n\nYou can also use the Bruno CLI to import WSDL files:\n\n```bash\n# Import WSDL file to a directory\nbruno import wsdl --source service.wsdl --output ~/Desktop/soap-collection --collection-name \"SOAP Service\"\n\n# Import WSDL from URL\nbruno import wsdl --source https://example.com/service.wsdl --output ~/Desktop --collection-name \"Remote SOAP Service\"\n\n# Import WSDL and save as JSON file\nbruno import wsdl --source service.wsdl --output-file ~/Desktop/soap-collection.json --collection-name \"SOAP Service\"\n```\n\n## Supported Formats\n\n- **Postman Collections** (v2.1)\n- **Insomnia Collections** (v4 and v5)\n- **OpenAPI Specifications** (v3.0)\n- **WSDL Files** (Web Services Description Language)\n\n## Dependencies\n\n- `lodash` - Utility functions\n- `nanoid` - UUID generation\n- `js-yaml` - YAML parsing\n- `xml2js` - XML parsing for WSDL\n- `@usebruno/schema` - Schema validation\n\n<!-- ... +15 more modular skills available via /api/skills/usebruno/bruno?path={path} -->\n</yakaai_skills>"}