# Orgbench

## Overview

Orgbench is an IntelliJ IDEA Tool Window for comparing Salesforce custom-field metadata between two locally authenticated `sf` aliases and selectively copying source differences to the target org. It uses the installed Salesforce CLI for authentication and operations; the plugin does not implement OAuth or ask for Salesforce credentials.

The first screen lets you pick a metadata type and build a session manifest of non-namespaced components across one or more types before comparing. **Objects & Custom Fields** is one item: selecting a custom object there compares both the object definition and that object's custom fields, so you never have to pick the same object twice, while a standard object contributes only its custom fields. A deployment runs in dependency waves — custom objects deploy before the custom fields that depend on them, so a source-only object can be created in the target in the same deployment as its fields. An explicit FLS transfer mode can also synchronize field-level security after a field is deployed successfully, or without redeploying metadata when the field already matches the target.

The plugin bundles a native core, `sf-core-server`, and talks to it over a JSON-RPC 2.0 stdio protocol. Bundled native core binaries ship for macOS arm64, macOS x64 and Windows x64; Linux is planned. A VS Code extension provides a second thin client of the same `sf-core-server` core, packaged as a platform-specific VSIX; see `vscode/README.md`.

## Requirements

- IntelliJ IDEA 2024.2+.
- A Java 17-compatible IDE runtime for the plugin UI. The bundled native core (`sf-core-server`) needs no JRE.
- A supported native platform: macOS arm64, macOS x64, or Windows x64 (Linux is planned).
- Salesforce CLI (`sf`) installed. The plugin checks the IDE process `PATH` and the standard macOS/Linux user and system install locations. On Windows it looks for `sf.exe`, `sf.cmd`, `sf.bat` and `sf` in `PATH` and in `%LOCALAPPDATA%\sf\client\bin`.
- Two authenticated aliases that resolve to different Salesforce org IDs.
- Source and target users with the API, metadata, and data permissions needed by the selected operations.
- Target objects must either already exist or be explicitly selected as CustomObject rows in the same deployment; a source-only custom object is created in the target before its fields deploy.

Check the CLI and aliases in a terminal before opening the Tool Window:

```bash
sf version --json
sf org list --json
```

If aliases are not configured yet, authenticate each disposable or approved org explicitly:

```bash
sf org login web --alias source-dev
sf org login web --alias target-qa
```

Restart IntelliJ IDEA after changing `PATH`; a GUI-launched IDE can inherit a different environment from an existing terminal. On macOS the plugin also detects `~/.local/share/sf/client/bin/sf`, `/opt/homebrew/bin/sf`, and `/usr/local/bin/sf`, so a standard Salesforce CLI installation does not depend on the GUI application's reduced `PATH`.

## Install from Disk

1. Obtain `orgbench-0.1.1.zip`, or build it as described below. Do not unpack the ZIP.
2. In IntelliJ IDEA, open **Settings/Preferences | Plugins**.
3. Open the gear menu, choose **Install Plugin from Disk**, and select the ZIP.
4. Restart the IDE if prompted.
5. Open **View | Tool Windows | Orgbench**.

The Tool Window checks `sf version --json` when it loads. It then discovers local aliases. A missing or incompatible CLI is reported with remediation text; choose **Restart** after correcting the prerequisite. An empty alias list is reported with remediation text too; choose **Reload Orgs** after authenticating an alias.

## Workflow

1. Select **Source**. The **Source** and **Target** dropdowns support type-to-search: with a dropdown focused, type a case-insensitive alias prefix to jump to matching aliases, the same case-insensitive prefix matching the component and field filters use. Pick a **Metadata Type** from its dropdown (Objects & Custom Fields, Custom Metadata Types, Platform Events, Apex Classes, Apex Triggers, Lightning Web Components, Aura Components, Visualforce Pages, Page Layouts, or Flows); the plugin loads customizable, non-namespaced components of that type into a filterable table. The dropdown is grouped by metadata category and lists only the categories you enabled in the settings; out of the box only the Core category is enabled, so the list is exactly the one above. Enabling a further category adds its types under their own group heading; thirteen categories are available beyond Core — Objects & Fields, UI & Navigation, Code, Automation, Security & Sharing, Integration & Auth, Service Cloud, Experience Cloud, Analytics, Sales Cloud, Email & Templates, Localization and Mobile — and together they carry 162 further metadata types, so with every category enabled the dropdown offers 172 types in fourteen groups. Typing in the dropdown searches those labels by substring in the IntelliJ plugin unconditionally, in the VS Code extension only when the combined list exceeds 11 types. If the type you were looking at belongs to a category you just switched off, the screen moves to the first type still on the list rather than showing rows of a type the dropdown no longer offers. Custom Metadata Types (`__mdt`) and Platform Events (`__e`) are object-form definitions listed by their name suffix; their fields are compared and deployed under Objects & Custom Fields like any other object's fields. Type a case-insensitive prefix in the **Label** or **API Name** filter cell to match that column (a trailing `*` is optional); check individual rows, use the header checkbox in the **Select** column for select-all-visible, or use **Select All** and **Clear**, which apply only to the currently visible, filtered components. The selections you make across metadata types accumulate into one session manifest — switching **Metadata Type** does not discard a prior type's selection, and a summary line reports how many components are selected per type. **Export Manifest…** and **Import Manifest…** next to **Select All**/**Clear** round-trip that whole session manifest through a `package.xml` file; see *Manifest Export and Import* below.
2. Select **Target**. Different aliases are still rejected if they resolve to the same underlying org ID.
3. Select at least one component and choose **Compare**. Source and target metadata are retrieved into separate temporary projects.
4. Review the `Select`, `Type`, `Component`, `Change`, and `Diff` columns. A field row whose target object is missing becomes selectable once the table also carries that object's deployable `CustomObject` `Add` row; you still have to select that row too, or the deployment is rejected before it starts. Custom `CustomObject` rows are deployable; a standard object contributes no definition row at all, because only custom objects can be deployed.
5. Each column filters independently from a filter row beneath the headers. Type a case-insensitive prefix in the **Type** or **Component** cell to match that column `like value*` (a trailing `*` is optional). Use the **Change** multi-select to pick any subset of `Add`, `Update`, and `No change`; by default only `Add` and `Update` are checked. The **Select** column's filter cell is a tri-state checkbox: checked shows only selected rows, unchecked shows only unselected rows, and the greyed (indeterminate) default applies no selection filter. The separate header checkbox still selects or clears only visible, eligible rows and becomes indeterminate for a partial visible selection.
6. Use **Back** to close the comparison and return with the current Source, Target, per-type component catalog, and session manifest preserved. Alternatively, select fields, choose **Do not transfer FLS**, **Additive FLS**, or **All FLS** from **FLS Mode**, and choose **Next**. In **Do not transfer FLS**, confirm the source, target, and metadata-field count before deployment; cancelling the confirmation leaves the review unchanged. In **Additive FLS** or **All FLS**, **Next** instead opens a container-selection screen listing every eligible container with its **Existing**/**New** presence column, filterable through the **Presence** column's multi-select (both shown by default); **Select All**/**Clear** apply only to the currently visible, filtered containers. Deselect any selectable container to exclude it, or **Back** to return to the review. Choosing **Next** there is the confirmation step for the FLS path.
7. During comparison or deployment, IntelliJ shows a cancellable background task. **Cancel** stops new work and asks the active CLI process to terminate when possible.
8. Read the field and FLS result tables. In the FLS result table, the **Status** column filter hides `Skipped` rows by default; check `Skipped` in that column's filter to reveal them alongside `Updated`, `Failed`, and `Not Attempted`. **Show Details** and **Copy** expose only sanitized diagnostics. **New Comparison** returns to a fresh alias selection.

## Git Source and Target

Each side of a comparison is chosen independently: a Salesforce org **or** a local git repository (an absolute path plus a branch). All four combinations work — org to org, org to git, git to org, git to git. A repository side is read through the SFDX layout declared by its `sfdx-project.json`: every `packageDirectories` entry is scanned in file order, and on a duplicate component the first entry wins. A source repository without `sfdx-project.json` is rejected; a target repository may be empty or not yet an SFDX project, in which case the plugin creates a minimal `sfdx-project.json` and a `force-app/main/default/` skeleton inside the same commit. A target repository that already has an `sfdx-project.json` the plugin cannot parse is a hard error instead — the skeleton never overwrites an existing manifest.

Branch reading follows one rule: the current branch is read from the working tree, so uncommitted edits are part of the comparison, while any other branch is read from the git objects. The branch dropdown lists local and remote branches; picking `origin/foo` as a target means "create or use the local `foo` from it".

Deploying to a repository writes the selected components into the layout, stages exactly those paths and makes one commit — the deploy ends in a local commit, pushing is up to you, and the plugin never checks out a branch in your repository: a branch other than the current one is written through a temporary `git worktree` that is removed afterwards, and when the target branch is the current one only the written paths are committed, so unrelated modified files stay out. The "+" button next to the branch dropdown names a new branch, and that branch is created only by the deploy commit — cancelling the session leaves nothing behind. Deploying a bundle (LWC/Aura) to a repository target can delete files: a file left over inside that bundle's directory from a previous version is removed, and the deletion is part of the same commit.

With a repository target there are no deploy modes and no Apex test screen: the write is atomic by construction and the Salesforce CLI is not involved at all. Dependency auto-selection needs the Tooling API and is therefore unavailable for a repository source, with one exception: when the source is an org (whether the target is an org or a repository), a Page Layout's own dependency on its custom fields is read straight from the layout XML and auto-selected without the Tooling API. When the source itself is a repository, this offline layout auto-select is unavailable too, exactly like every other dependency. Two selection rules need no Tooling API at all and therefore work with a repository source as well: a field row whose object is missing from the target auto-selects that object's `Add` row, and checking that object's `Add` row auto-checks the object's own field rows. The review screen still warns about every other Tooling-sourced dependency it cannot compute for a repository source. FLS transfer does work with repository sides — Profile and Permission Set documents are read from and written to `profiles/` and `permissionsets/` files in the same commit, and a Profile file is only ever updated, never created.

GitHub endpoints are not yet supported: the entry is visible but disabled, and the core rejects it.

## Manifest Export and Import

The selection screen round-trips the session manifest through a Metadata API `package.xml` file. Both actions run entirely in the plugin core: the client only shows a native file dialog and hands the core a path.

**Export manifest** writes the current session manifest — including the checkboxes of the type you are looking at right now, which are committed into the manifest first — to the file you pick in a save dialog (default name `package.xml`). It uses the same API version as the retrieve path and the standard `<types>`/`<members>`/`<name>` layout, so `sf project retrieve start --manifest package.xml` accepts it unchanged. Objects & Custom Fields, Custom Metadata Types and Platform Events merge into the single `CustomObject` section the Metadata API expects; Apex Classes, Apex Triggers, Lightning Web Components, Aura Components, Visualforce Pages, Page Layouts and Flows each get their own section. Field selections are exported at object granularity: a selected object is written as one member, never as `Object.Field`.

**Import manifest** reads the `package.xml` you pick and replaces the session manifest with it — the file is the source of truth, so export the current selection first if you want to keep it. Every member is validated against the source org and nothing is skipped silently: the screen reports how many components were imported plus one warning per skipped member. A `<members>*</members>` wildcard is expanded to the full source-org listing of that type. A `CustomObject` member imports the object *and* its fields, matching Metadata API semantics, and `__mdt`/`__e` members are routed to Custom Metadata Types and Platform Events by their suffix; a standard object imports only its fields, because a standard object definition can never be deployed. A `CustomField` member written as `Object.Field` is widened to its object with a warning, because the manifest's granularity is the object. A member the source org does not list, a namespaced member, and an unsupported manifest type (`Profile`, `ConnectedApp`, and the like) are each reported and skipped — namespaced members are never imported, in line with the rest of the plugin.

The importer rejects a `DOCTYPE` declaration, a file larger than 5 MB, and any XML whose root element is not `Package`. A rejected file leaves the session manifest and the current metadata type untouched, and so does a failed source-org listing.

## Comparison Rules

Components match by metadata type and API name (fields additionally by their `SObject Type`). The plugin shows:

- `Add` when the source field is absent from an existing target object;
- `Update` when a matching field differs in a supported attribute;
- `No change` when supported attributes are equal;
- no row for target-only fields, because the plugin never deletes fields.

The Change multi-select controls which source rows are visible: check any subset of `Add`, `Update`, and `No change`. It defaults to `Add` and `Update` checked, so `No change` rows are hidden until you check that value. A `No change` row is never a metadata deployment candidate. It becomes selectable only in **Additive FLS** or **All FLS** mode, and returning to **Do not transfer FLS** immediately deselects all such rows.

Updates compare these attributes in deterministic order: type, formula, default value, lookup or master-detail target, length, precision, scale, and label. A short diff is rendered target-to-source, for example `Length: 40 -> 80; Label: Old -> New`.

Namespaced metadata is ignored. This applies to namespaced objects and fields, including managed-package fields such as `namespace__Field__c`; the MVP does not try to detect a packaging org in which that namespace is under development. Standard fields are not copied.

When an object is absent from the target, its source fields are displayed as `Add` with `Target object is missing`. Those field rows become selectable once the table also carries the object's deployable `CustomObject` `Add` row; otherwise they stay disabled. Selecting the field still requires selecting that object row too: if you select such a field without selecting its object row, the deployment is rejected before it starts with `Missing target object 'X__c': select its object row or deselect its fields.`

`CustomObject` metadata is compared like any other type — `Add`, `Update`, and `No change` rows are computed the same way. Only a custom object (`__c`) gets a definition row: a standard object selected in **Objects & Custom Fields** contributes its custom fields alone, mirroring the rule that standard fields are never copied.

Custom Metadata Types and Platform Events are the same object form on the Metadata API (a `CustomObject` component), so they deploy in the object wave before their fields exactly like custom objects: a source-only `__mdt` or `__e` object can be created in the target in the same deployment as its fields. A `Custom Metadata Types` row that is not a `__mdt` object is disabled with `Only custom metadata types can be deployed.`, and a `Platform Events` row that is not a `__e` object with `Only platform events can be deployed.` Custom Metadata records (`Type.Record`) are covered too, as Custom Metadata Records under Objects & Fields: unlike the object definitions above, a record is compared as the whole document and deploys in the last wave with the other additional types.

Apex Classes and Apex Triggers are a code type: each component is its source body (`.cls`/`.trigger`) plus an XML sidecar carrying `apiVersion` and `status`. Comparison covers the body text and the sidecar attributes together, so an `Update` row can come from a body change, a sidecar change, or both. The row diff view shows the source body file. Visualforce Pages have the same shape: the `.page` body plus a `.page-meta.xml` sidecar carrying `apiVersion` and `label`.

Lightning Web Components and Aura Components are bundles: one component is a whole directory (`lwc/<Name>/`, `aura/<Name>/`), so they are compared by composition rather than by attributes. An `Update` row means the set of files or the content of at least one file differs, summarized as `N file(s) added, M removed, K changed` (terms that are zero are omitted); the row diff view shows the whole bundle as one concatenated document, each file preceded by a `=== <relative path> ===` separator line. Deploying a bundle replaces that bundle in the target completely: a file that exists only in the target's version of the same bundle is gone after the deployment, because the Metadata API treats a bundle as one indivisible component. The plugin still never deletes a target component: a bundle that exists only in the target produces no row and is never touched.

Page Layouts are decomposed: one row is not a whole layout but a single layout **element** — a field of a section or a related list. A layout that is missing from the target produces one `Add` row for the whole document; a layout that exists in the target produces rows only for the elements that differ, and matching elements produce no row at all. An element that exists only in the target never produces a row and is never removed. Position is never a difference: the plugin transfers a new element into the position its source neighbours dictate, but it never re-orders elements that already exist in the target. A selected related-list `Update` row replaces the whole related list with the source version — its columns, sort field, sort order and buttons are all overwritten, so anything present only in the target's copy of the same related list is gone after the deployment; everything else in the layout — the layout's own buttons, quick actions, the highlights panel and the mini layout — is preserved by construction: existing lines are never rewritten. Layouts of standard objects are compared and deployed. The row difference view shows the whole layout document of both orgs, the same document for every row of that layout. A layout whose own name contains `::` is excluded from the comparison with a visible warning, because the plugin uses `::` to address layout elements. A layout whose metadata cannot be parsed is not softly excluded: reading that layout's metadata fails the operation outright, the same way an unreadable metadata file fails any other type.

Types outside the core set — every type of the thirteen additional categories — are compared as the whole document, not attribute by attribute: one row is one component, and the row difference view shows both documents in full. Their component name is the `fullName` the Salesforce CLI reports, so a tab of a custom object is listed as `Invoice__c` and a quick action as `Account.New_Note`. Namespaced components of these types are excluded like every other namespaced component, and a name that is a custom object's own API name is not mistaken for a namespace prefix. Types that Salesforce stores inside an object's folder — list views, record types, validation rules, field sets, compact layouts, business processes, indexes, sharing reasons and web links — are listed under a composite `Object.Name` key such as `Account.AllAccounts` or `Invoice__c.My_View`, and their file lives in that object's folder. Children of a standard object are compared and deployed like any other child, because a list view or a validation rule is the admin's own configuration rather than the object definition; the standard object's own definition is still never deployed. Selecting a child of a custom object that is missing from the target auto-checks that object's definition `Add` row as a visible dependency — you see it in the table before the deployment and you can uncheck it. The object's own custom-field rows are auto-checked right alongside it, because a list view's columns or a validation rule's formula name those fields, and the target rejects the child if the object is created without them; each field row can be unchecked on its own, just like the object row. Like the offline layout auto-select, this widening runs when the source is an org; with a repository source neither the object's row nor its field rows are added automatically — select them yourself. Types that Salesforce stores many-to-a-file — assignment rules, auto-response rules, escalation rules, the three kinds of sharing rule and custom labels — are listed one row per rule or label, not one row per file: a rule is keyed `Object.Name` such as `Lead.Web_Leads`, and a custom label by its own name, because a single `CustomLabels` file holds every label in the org. The three sharing-rule types share one physical file per object and are told apart by the element that carries them, so an owner rule and a criteria rule of the same object are two independent rows that deploy together into one file.

Flows are the opposite of Page Layouts: one row is one whole flow. Four header attributes are shown as a short diff — label, status, API version and process type — and any other difference in the document is reported as `Definition changed`. Version numbers are never compared, because the flow document does not carry one: the Metadata API decides which version it hands over, so the active version of a flow is compared, and when a flow has no active version its latest version is compared instead. An `Update` row can therefore mean that a different version is active in each org rather than that somebody edited the flow. The `<status>` value is transferred verbatim, so deploying a flow whose source status is `Active` activates it in the target; that status is visible in the row before you deploy, and **Validate only** is available as a dry run. Deploying a flow creates a new version in the target and never deletes or deactivates an existing version. A flow that exists only in the target produces no row and is never touched. This comparison has a known limitation for a git source compared against an org target: the plugin only normalizes line endings, so any other formatting difference between the repository's copy of a flow and the org's own serialization — indentation, element order, manual edits — still reports as `Definition changed`, and deploying does not clear it, because the target org re-serializes the deployed document on its own terms; every redeploy from that repository copy then adds another version to the target. The other three endpoint pairs are unaffected: org to org passes through the same Metadata API retrieval on both sides, so their serializations match by construction; org to git and git to git write exactly the retrieved or merged bytes, so the compared bytes are exactly the deployed bytes.

## Dependency Auto-Selection

When you check a row in the comparison table, the plugin also checks every component that row depends on and that is missing from the target. The dependency data comes from the Salesforce Tooling API object `MetadataComponentDependency`, queried on the source org only; that object is a Salesforce Beta feature, so the plugin treats it as best effort.

The cascade also runs the other way for a new object: checking a new object's `Add` row also checks every custom-field row of that object, while checking a single field still checks only that field's object and not its sibling fields. Auto-checked rows are marked **added as dependency** together with the row that pulled them in, and you can uncheck any of them — the auto-selection is a suggestion, never a lock. Unchecking a row also unchecks the rows that were only there because of it; a row you checked yourself is never unchecked automatically. Dependencies are followed recursively, so checking one field can bring in its object and the Apex class they reference.

Only rows the target is missing (`Add` rows) are ever auto-checked: an `Update` or `No change` dependency already exists in the target and needs no transfer. A dependency the plugin cannot transfer — a standard component, a packaged or namespaced component, or any component type outside the eleven the dependency graph covers (`CustomLabel`, `RecordType`, `WorkflowRule`, and the like) — is simply not auto-checked and never appears in the table. The one exception is a Flow dependency whose metadata name came from the Tooling API as its display name instead of its API name: it could not be matched automatically, so the row reports it, and you should make sure it exists in the target org or select it manually. Page Layouts are one of the eleven the dependency graph covers, but the Tooling dependency graph carries no layout references, so this feature never pulls a layout in as another component's dependency; a layout element's own dependency on a custom field is instead auto-selected offline, without the Tooling API, as described above. That offline auto-select also covers a whole-layout `Add` row: when the layout itself is missing from the target the comparison shows one row for the whole document, and that row carries the dependencies of every element it contains. A related list element is covered too: when the list is named `{child object}.{lookup field}`, the lookup field itself (if it is a custom `__c` field), every column of the list written as a plain custom field API name, and the sort field are auto-selected on the child object. A column written as a UI name (`CONTRACT.NAME`, `OWNER_NAME`) or as a person-account field (`__pc`) is left alone, and a standard related list — a name without a dot — has no dependencies at all.

If the Tooling API object is unavailable — the Beta feature is off in the org, or the query fails — the comparison still completes exactly as before: no Tooling-sourced row is auto-checked and the screen shows `Dependency auto-selection is disabled` with the reason. The two offline rules still apply — a field still pulls in its missing target object, and checking a new object still checks its own field rows. The plugin never blocks a comparison because of this feature. Because the Salesforce dependency graph has known gaps, a missing edge does not prove there is no dependency; the deployment validation stays the source of truth.

The comparison may pull extra components into itself to resolve a dependency; the screen lists them under **Added to comparison as dependencies**. That expansion lives only in the comparison — **Back** returns your original selection screen untouched. Only the rows something you asked for actually needs are shown: a component pulled in this way gets a row when it is reachable over dependency edges from a row of your own selection, and that row's summary then names the row that required it. An object that ends up with no rows at all drops out of that list as well, so the list never sends you looking for a row that is not in the table. Everything else the expansion downloaded stays out of the table — to work on such a field, select its object on the selection screen explicitly.

## Row Difference View

Double-click any row in the comparison table to open a side-by-side diff of the
component's Salesforce DX source-format file: the source org on the left, the
target org on the right. The view is built from the metadata already retrieved
for the comparison, so it makes no additional org calls. For an `Add` row (or a
row whose target object is missing) the right pane shows that the component is
not present in the target. Complex components such as a `CustomObject` show only
their object-level source (the source format keeps each field in its own file),
never an enumeration of their fields.

## Progress Details

While an operation runs, the status text is joined by a live progress line: a spinning
indicator, the current phase, the step counter where the number of steps is known, and the elapsed time
since the operation started, followed by a bar that fills when the step count is known and
runs indefinitely when it is not. The facts come from the core — it is the only layer that
knows which Salesforce CLI command is running — and the line carries metadata type names,
org aliases and counters only; CLI output is never shown there.

Examples: `Loading objects from dev · 4s`, `Querying dependencies — 3 of 7 · 12s`,
`Deploying Custom Fields — 2 of 5 · 1m 8s`. The step count is a live estimate: while
dependencies are queried it can grow as the core splits an oversized batch, so `3 of 7`
may be followed by `4 of 9`.

Some steps run without leaving the screen you are on. The screen that started such a step
stays where it is, disables its buttons and shows the same live line, and **Cancel** stays
available. There are three such steps today: listing the Apex test candidates after **Deploy**
on the comparison screen (when the package contains Apex or Flow and the target is an org),
matching FLS containers
after **Deploy** on the comparison screen (when FLS mode is not "Do not transfer FLS"), and
matching FLS containers after you confirm the tests on the Apex test screen.

The line is optional. In the IntelliJ plugin the checkbox lives in
**Settings | Tools | Orgbench**; in the VS Code extension it is the setting
`orgbench.showProgressDetails`. Both are on by default. Clearing the checkbox stops
the line: nothing new is drawn, and a line already on screen disappears with the next progress event or state change —
inside one long CLI phase that can take a minute; the core stops sending progress the next time the core process starts —
the plugin never restarts the core to apply this setting, because that would discard the
current comparison. If no progress event ever arrives (a core that does not implement the
channel, or the channel switched off), the screens behave exactly as they did before: the
static status text and the Cancel button.

## Settings and Session Log

The FLS transfer mode and the deployment mode are settings, not controls on the review screen. In
the IntelliJ plugin they live in **Settings | Tools | Orgbench**, reachable in one click from the
gear button in the tool window; in the VS Code extension they are `orgbench.flsMode` and
`orgbench.deployMode`, reachable from the gear icon in the view title. Because they are settings,
they no longer reset with every new comparison: a mode you chose once stays chosen. The review
screen therefore always shows what the next deployment will do as a read-only line, for example
`FLS: Additive · Deploy: Validate only`.

The core writes a session log — one JSON object per line — that answers two questions: how long an
operation took and what it was made of, and what exactly failed when you need to file a bug
report. Each line carries a timestamp, an event kind and an operation id. The line that answers
"where did the two minutes go" is `operationFinished`: it carries the operation kind, its duration,
how many Salesforce CLI calls it made and how many milliseconds were spent inside them. A `cli`
line records one `sf` invocation: the subcommand, the target org alias, the duration, the exit
code, the byte lengths of stdout and stderr, and — on failure — the same sanitized diagnostic the
UI shows. An `operationCancelled` line covers two distinct reasons and does not distinguish them in
its fields: either the user chose Cancel, or
the core force-aborted the operation after its cleanup failed.

**Metadata categories** is a setting of the same kind: it decides which categories of metadata
types the **Metadata Type** dropdown offers. In the IntelliJ plugin it is a list of checkboxes on
the same settings page; in VS Code it is `orgbench.metadataCategories`, an array of category ids.
By default only the Core category is enabled, so the dropdown lists exactly the types the plugin
offered before the metadata-type expansion. Changing it takes effect on the screen you are already
looking at. Queues, public groups and roles are the three types Security & Sharing does not offer yet:
their documents carry a member list of the source org, and the plugin does not yet know how to create
such a component without it. Other types whose documents reference users or profiles are offered as
they are, and a deploy that names a user the target does not have fails that one component in Best-effort mode; in Validate only or All or nothing the whole package fails, as everywhere else.
Disabling a category does not clear a selection already made in a type it hides: that selection
stays in the session manifest and is still sent to Compare, Deploy and Export Manifest, even
though the type itself is gone from the dropdown. A line below the manifest summary names every
such hidden type and how many of its components are still selected, and choosing **Compare** while
any such selection exists shows a confirmation listing those types and counts before continuing.
This is true of both clients, not only the IntelliJ plugin.

Three levels are available, chosen in the same place as the modes (`orgbench.logLevel` in VS
Code):

- **Off** — nothing is written; neither the log file nor its folder is created, and the
  retention sweep that deletes older logs does not run either.
- **Standard** (the default) — every kind of record listed above.
- **Detailed** — the same plus the full Salesforce CLI argument list of each call.
  Detailed additionally records the SOQL request text, because it is part of those arguments, and
  for the same reason it records local file paths such as the retrieve manifest.

The log **never records a token**, and it records no response body either — with one deliberate
exception: when an operation fails, the log keeps
the same sanitized diagnostic the UI already shows, capped in length. For a failed Salesforce CLI
call that diagnostic quotes an excerpt of the CLI response; for a failure with no CLI call behind
it — a malformed `package.xml`, say — it is the message you see on screen. Otherwise only the byte
lengths of stdout and stderr survive, and every recorded argument passes the same redaction the
plugin applies to displayed diagnostics. Changing the level takes effect immediately, without
restarting the core process.

The file lives under the plugin's own temporary directory, in `orgbench/logs`, and is named
`session-<timestamp>-<pid>.jsonl`. The plugin keeps the five most recent session logs and deletes
older ones; it removes only files matching that name pattern and never anything else in the
folder. A session log is capped at 8 MiB, after which a final `logTruncated` line is written and
the session stops logging. The settings page shows the current path and offers **Open Log Folder**
and **Copy Path**; VS Code offers the same two as commands in the view's overflow menu. The file
appears with the first record, so a freshly started session with nothing logged yet has a path but
no file.

## Deployment Behavior

Only the selected component XML is copied into a clean attempt directory. The plugin does not deploy complete object metadata or unrelated fields; deploying a `CustomObject` writes only that object's `.object-meta.xml`, never touching its existing fields or other components in the target.

Creating an object (an `Add` row of Custom Objects, Custom Metadata Types, or Platform Events) deploys a minimal object document synthesized from the compared attributes plus, for Custom Objects, the required `nameField`, instead of the source org's full file: search layouts, list views, web links, and action overrides reference components that do not exist in the target yet, and Salesforce rejects the whole object because of them. This holds in all three deploy modes — the wave path and the single-package path build the object document the same way. If the source Custom Object carries no `nameField`, the plugin refuses to package it instead of sending the full file: in Best-effort that object's row fails on its own and its fields are `Not Attempted`, while on the single-package path the whole deployment fails before any deploy call. An `Update` row still deploys the source file as it is, and the **Row Difference View** always shows the real source content.

Deployment runs in dependency waves by metadata type: custom objects (wave 1) deploy before custom fields (wave 2), Apex Classes (wave 4) deploy before Apex Triggers (wave 5), the UI layer — Lightning Web Components, Aura Components, and Visualforce Pages — deploys in wave 6, Page Layouts deploy in wave 7, Flows deploy in wave 8, and the remaining metadata types deploy last, in wave 9. Within the object, field, UI, layout, flow, and remaining-types waves, components are sent in batches of at most 10. A Salesforce component failure causes the batch to split recursively so unaffected components can still succeed. Isolated component failures are retried once after the others, allowing a dependency deployed later in the selection to satisfy a formula or relationship field. Separate successful batches remain committed; there is no cross-batch rollback. If a selected object's `Add` fails to deploy, its fields are marked `Not Attempted` because their target object failed to deploy. Because a layout's merged target document is prepared right before deployment, a failure to prepare a selected layout stays isolated to that layout's own rows in Best-effort mode and against a git target, but in Validate only or All or nothing against an org target the same failure fails the whole package before any deploy call.

Apex Classes and Apex Triggers each deploy atomically within their wave: all selected components of that type are sent as one Metadata API deployment, with no batching into groups of 10 and no isolated-failure retry, because Apex compilation validates the whole set together. Apex rows are selectable for any target org. Before deployment, the test screen opens when the package contains Apex or Flow — an Apex Class, an Apex Trigger, or a Flow anywhere in the selection — and the target is an org; a git target never opens it. In that case **Next** opens a dedicated **Select Apex Tests** screen. That screen offers only a list of candidate classes with checkboxes — the candidate list combines the Apex classes already present in the target org (namespaced classes excluded) with the `@isTest` classes being deployed in this very selection, marked **This deployment** or **Target org**; classes whose name contains `Test` are listed first, and the list is searchable. Deploy is always enabled: an empty selection is a legal choice — a sandbox target then deploys with no tests run at all, but a production target still runs its own local Apex tests and requires 75% coverage when no test level is requested. Checking one or more classes runs `RunSpecifiedTests`; the checked test class names are passed to the Salesforce CLI as separate arguments (`--test-level RunSpecifiedTests --tests <Class>` repeated per class), and only names from that published list are accepted. Running every local test in the org is not offered as an option. In Best-effort mode, a non-empty test selection merges the Apex Class and Apex Trigger waves into one atomic Metadata API deployment, so the chosen tests run once instead of twice; the tradeoff is that a failing class then rolls back the triggers too. An empty selection leaves the two waves separate, as elsewhere. Flow waves in Best-effort never run Apex tests, regardless of the selection. When a deployment is rejected for insufficient code coverage, the target's own coverage warnings are read from the response and matched to the class they name: that class's own row gets a personal message with just its warning text, for example `Code coverage: Test coverage of selected Apex Class is 0%, at least 75% test coverage is required`, while any other row in the same rejected batch gets the shared rejection text — the same reply, still naming every flagged class — for example `Code coverage: MyClass: Test coverage of selected Apex Class is 0%, at least 75% test coverage is required` — instead of a bare status word.

Within the metadata waves themselves, the plugin distinguishes two kinds of failure. A **target rejection** is Salesforce answering with a terminal verdict that rejects what was sent — an Apex compilation error, a failed test, content the org refuses, a component-level verdict that arrived malformed or that could not be mapped back to exactly one requested component. It fails only the components of that one deployment call: every row of that batch is `Failed` with the sanitized rejection text, the remaining batches of the same wave are still sent, and every later wave still runs. When a target rejection carries no per-component detail, every component of that call gets the same message, and the plugin does not split the batch to find the culprit. A **connection failure** is the target never answering at all — authentication, authorization, CLI, network, an unrecoverable timeout, a malformed or missing response, a job that never reaches a terminal state, or a workspace failure. It is fatal: the current attempt is `Failed`, later components and later waves are `Not Attempted`, and no new batch is scheduled. Cancellation stops everything the same way. An object wave is the one contained case of a connection failure (cancellation still stops everything): a fatal failure there still stops that wave immediately, the fields of its failed objects are `Not Attempted` because their target object failed to deploy, but the remaining waves — the other object groups, the fields of objects that already exist in the target, Apex, and the UI layer — still run. Validate only and All or nothing are unaffected by this split: they make a single deployment call for the whole package, so both kinds of failure produce the same all-rows-failed report there. The plugin does not use `--ignore-errors`, delete target fields, or perform a destructive deployment; it creates a target object only when you explicitly select that object's deployable `CustomObject` row. The separate FLS step that follows the waves has its own, more forgiving failure handling — see **FLS Transfer Modes**.

At comparison time, the plugin records both aliases, usernames, and org IDs. Before deployment it reloads the aliases and rejects the operation if either alias no longer resolves to the reviewed username and org ID. Salesforce operations within the reviewed session use those stable usernames rather than trusting a later alias reassignment.

The **Deploy mode** setting offers three mutually exclusive choices, defaulting to the current behavior:

- **Best-effort deployment** (default) keeps the wave pipeline described above: batches of at most 10 with recursive split, isolated-failure retry, per-object skip, an inter-wave stop on a connection failure that an object wave contains, a target rejection contained to its own deployment call, and a separate FLS deployment after the waves, whose own non-cancellation failures are contained rather than fatal to the metadata report (a cancellation still stops the whole operation; see **FLS Transfer Modes**). Partial success is allowed.
- **Validate only** builds the same package but calls the CLI with `sf project deploy start --dry-run`, so nothing changes in the target org. Results report **Would succeed** / **Would fail** instead of Succeeded / Failed.
- **All or nothing** sends every selected component of every type plus merged FLS in one atomic Metadata API deployment; any failure rolls back the whole package (`rollbackOnError` is on by default), and non-culprit rows are reported failed with a transaction-rolled-back message.

Validate only and All or nothing take a single-package path (one deployment for the whole selection); Best-effort keeps the multi-call wave path. The plugin never uses `--ignore-errors` in any mode and never deletes target components. The test screen offer is the same in all three modes — it opens once, before the selected mode starts, whenever the package contains an Apex Class, an Apex Trigger, or a Flow and the target is an org — but what the chosen tests attach to differs. In Best-effort, a non-empty selection merges the Apex Class and Apex Trigger waves into one deployment so the tests run once; Flow waves never run them. In Validate only, the single package carries the selection and really runs the chosen tests (nothing is written to the target), including against a Flow-only package. In All or nothing, the single package carries the selection too, and it rolls back the whole package when a test fails, including against a Flow-only package. When tests ran, the results screen adds a `Tests: N run, M failed` line, and failed test names with their sanitized messages appear in the failing operation's details.

## FLS Transfer Modes

The mode is chosen in the settings (see **Settings and Session Log**), not on the review screen, and it applies to every deployment until you change it.

The **FLS mode** setting offers three choices:

- **Do not transfer FLS** is the default and deploys selected field metadata without retrieving or changing FLS.
- **Additive FLS** transfers source access for selected fields without revoking existing target access. Read and Edit are merged independently with OR semantics, so the result is the union of normalized source and target access.
- **All FLS** fully synchronizes each selected eligible field to normalized source access in every matched container, so it can grant or revoke access.

**Additive FLS** and **All FLS** include selected `No change` fields immediately and selected `Add` or `Update` fields only after their metadata deployment result is `Succeeded`. A selection containing only `No change` rows skips field deployment entirely. The plugin matches source and target standalone Permission Sets by metadata full name and Profiles by metadata full name. Namespaced permission containers and Permission Set Groups are excluded.

Choosing **Additive FLS** or **All FLS** and **Next** opens a container-selection screen listing every eligible container: **Existing** containers matched by kind and API name in both orgs, and **New** Permission Sets that exist only in the source and are eligible for creation. Each row shows its presence in an **Existing**/**New** column; filter it with the **Presence** column's multi-select, which shows both values by default. **Select All** and **Clear** apply only to the currently visible, filtered containers. Every selectable row starts checked — every Existing container and every New Permission Set; a New Profile is listed for information only, with its checkbox disabled, because Profiles are never created. Deselect any selectable container to exclude it from this deployment. A container that is left unselected is reported `Skipped` with "Not selected for FLS transfer."; this is an additional, user-driven `Skipped` reason alongside target-only, namespaced, and other ineligible exclusions, and it does not change any other result semantics.

The plugin retrieves both copies of each matched Permission Set/Profile and uses the target XML as the merge base. All selected fields for that container are accumulated into one metadata document. Only their `fieldPermissions` entries are merged according to the selected mode; unrelated target field permissions and all other target metadata nodes remain intact. Edit access implies read access, formula fields are normalized to `editable=false`, and required fields are skipped because Salesforce does not support FLS for them.

In **Additive FLS**, an absent or narrower source `fieldPermissions` entry never removes or reduces target access. In **All FLS**, an absent source entry means no read or edit access, so synchronization removes the corresponding target entry. Target-only containers are `Skipped`; the plugin never deletes a target-only container. A source-only Profile is also `Skipped`, because Profiles are never created. A source-only, non-namespaced Permission Set is classified **New**; when selected, the plugin may create a Permission Set in the target when it is missing there, populated with only the selected fields' field-level security — never a license, never any other permission, and never a Profile. Profiles that do not exist in the target are not created. A selected field whose desired access already matches the target is also `Skipped` because it requires no deployment.

**All FLS** can make destructive changes (access revocations) only for a unique, non-namespaced source-and-target container match by both kind and API name. Target-only, namespaced or otherwise ineligible, required-field, and no-op outcomes are `Skipped`; a source-only Profile is also `Skipped`, but a selected source-only, non-namespaced Permission Set is instead created in the target and reported `Updated`. Ambiguous duplicate container identities and Salesforce deployment rejections are `Failed`. In the FLS results table, the **Status** column filter hides `Skipped` rows by default; check `Skipped` in that column's filter to reveal them.

Changed Permission Sets and Profiles are first submitted together in one aggregate Metadata API deployment. A component-specific failure causes recursive splitting by container until the failing Permission Set/Profile is isolated; the plugin never splits one container's accumulated field changes into per-field deployments. A container is reported successful only after a deployment containing that component succeeds. Fatal authentication, authorization, CLI, network, workspace, cancellation, or response errors stop further scheduling. `Updated` means an actual permission-metadata change was deployed; no-op rows are `Skipped`. There is no rollback across separate successful fallback deployments.

Before any of that scheduling starts, the plugin has to read the Permission Set/Profile catalog itself — the SOQL query that lists candidate containers and the retrievable-metadata inventory that tells which of them can actually be retrieved. Each of those two read-only calls is retried once on a failed CLI response, the same one-shot idea as the metadata batch's own isolated-failure retry above; a cancellation is never retried, and it still stops the whole operation like any other cancellation. In Best-effort mode against an org target, if that early catalog read still fails after the retry, the FLS step does not take the metadata report down with it: every component deployed in the waves above keeps its own result row, and every field/container pair that would have gone through FLS is instead reported `Not Attempted`, with the sanitized failure message available in **Show Details**. In Validate only and All or nothing, and against a git target, the same catalog failure stops the whole operation before anything is deployed or committed.

The FLS result table's column filters do not change the counters. The **Status** column filter defaults to `Updated`, `Failed`, and `Not Attempted` checked and `Skipped` unchecked; check `Skipped` to include it, or uncheck any of the other three statuses to narrow the view further.

## Safety Boundaries

- Salesforce authentication remains in the local Salesforce CLI. The plugin does not request, persist, or display access tokens, refresh tokens, passwords, or auth URLs.
- CLI commands are executed directly as argument lists, not through a shell. Diagnostics are sanitized before display or copy.
- Namespaced objects, fields, Permission Sets, and Profiles are excluded. Ordinary custom names such as `Field__c` remain eligible.
- An object is created in the target only when its `CustomObject` `Add` row is checked in the comparison table — either by you or automatically as a visible dependency of a component you checked; the auto-check can be removed, and the object creation is always visible in the table before the deployment. Target-only fields are never deleted, and standard-object metadata is never deployed: the standard object's own definition and its standard fields are out of scope. Page Layouts and the types stored inside an object's folder (list views, record types, validation rules, field sets, compact layouts, business processes, indexes, sharing reasons, web links) are the deliberate exceptions — those of a standard object are compared and deployed, because they are the admin's own configuration rather than the object definition.
- Deploying one child of a container document — an assignment rule, an auto-response rule, an escalation rule, a sharing rule or a custom label — never removes the other children of the same container. The package carries only the children you selected, and the Metadata API leaves the rest in place; against a git target the plugin merges the selected children into the branch's own copy of that file, so a rule or a label that exists only in the repository survives the deployment.
- A selected related-list `Update` row replaces the whole related list with the source version — columns, sort field, sort order and buttons are all overwritten, so anything present only in the target's copy of the same list is gone after the deployment. This is the only place where a deployment removes content from a layout; everything the merge did not touch is preserved by construction, and elements that exist only in the target are never removed.
- Deploying a flow creates a new version in the target and never deletes or deactivates an existing version. The target's active version is replaced exactly when the source flow's `<status>` is `Active`, because the status is transferred verbatim and is visible in the comparison row before the deployment.
- Optional FLS synchronization is limited to selected metadata-identical fields plus selected fields whose metadata deployment succeeded. For an Existing container it merges only the selected `fieldPermissions` entries into target-based Permission Set/Profile metadata, and only for the containers the user selected on the container-selection screen; deselected containers remain untouched and are reported `Skipped`. The plugin may create a Permission Set in the target when it is missing there, populated with only the selected fields' field-level security — never a license, never any other permission, and never a Profile. Profiles that do not exist in the target are not created.
- The IntelliJ system directory is the explicit local filesystem trust boundary. The plugin parent is its direct child `orgbench`; operation workspaces below it contain no credentials. On Windows the plugin parent and descendants are created with owner-only ACLs. Workspaces are closed after handled success, failure, cancellation, **Back**, **New Comparison**, or project disposal. If cleanup fails, the error is surfaced and the owned workspace is retained for another cleanup attempt on reload or close.
- Importing a `package.xml` changes only the local selection. It never touches either org beyond the standard source-org component listings, never imports namespaced members, and rejects a `DOCTYPE` declaration outright; any failure leaves the previous session manifest in place.
- Separate successful field batches and FLS fallback deployments are not rolled back when later work fails. Always review both result tables before treating an operation as complete.
- Automated tests use fixtures and fakes; the default Gradle test task does not connect to a Salesforce org.
- Automated orchestration tests cover max-10 field batching, recursive field splitting, dependency retry, aggregate FLS deployment, container-level FLS fallback, fatal stop, and `Not Attempted` ordering. The manual guide deliberately validates one real field component failure without intentionally causing an authentication or network failure against a live org.

All four tables — object selection, field review, FLS container selection, and both result tables — share one filterable layout: a header row, an aligned per-column filter row beneath it, and the data. Column widths are saved globally per table in IntelliJ application properties after a header resize; the compact default sizes any Select column to its actual checkbox header, keeps narrow columns like Change and Status compact, and leaves the widest text column (Diff, Message) expanding. The filter row re-aligns each filter cell to its column as columns are resized. Saved table widths apply across projects and IDE restarts and are clamped to safe minimums when restored.

## Build

Use a JDK 17 runtime and the checked-in Gradle wrapper:

```bash
./gradlew test
./gradlew verifyPlugin
./gradlew buildPlugin
```

`./gradlew buildPlugin` also compiles the Rust core (`sf-core-server`) for macOS arm64, macOS x64 and Windows x64 and bundles all three under `bin/` in the archive. The Windows x64 binary is cross-compiled from a non-Windows host, which needs a one-off `cargo install cargo-xwin` plus `rustup target add --toolchain 1.97.0 x86_64-pc-windows-msvc`; on a Windows host the same Gradle task calls plain `cargo build`. The Rust core has its own checks:

```bash
cd core && cargo test --locked
cd core && cargo clippy --all-targets --locked -- -D warnings
```

The VS Code extension lives under `vscode/` with its own npm toolchain; `./gradlew check` also type-checks and runs its `node:test` suite, and `./gradlew buildVsixAarch64 buildVsixX64 buildVsixWin32X64` writes the platform-specific VSIX archives under `vscode/`.

`verifyPlugin` can download the configured IntelliJ compatibility target. The installable archive is written to `build/distributions/orgbench-0.1.1.zip`. Build and verification do not require an authenticated org and do not run the manual live-org scenario.

## Troubleshooting

### Salesforce CLI is unavailable or incompatible

Run `sf version --json` in a terminal. Install or update Salesforce CLI, restart the IDE, and choose **Restart**. The plugin automatically checks the IDE `PATH` plus standard install locations. If `sf` is installed in a custom location, add its directory to the environment used to launch IntelliJ IDEA.

### The Salesforce core process did not start

The plugin launches a bundled `sf-core-server` binary from its `bin/` directory. If the tool window reports that the core process could not start or terminated, confirm the platform is supported (macOS arm64, macOS x64, or Windows x64) and that the plugin ZIP was installed from disk without unpacking. Reinstall the plugin if the bundled binary is missing. On Windows the bundled binary is unsigned, so SmartScreen or the antivirus may quarantine it on first launch.

On Windows, three further messages come from the core refusing to trust its own temporary workspace rather than from a startup failure: `Could not read filesystem identity` means the project or IDE cache directory sits on a filesystem (FAT/exFAT, or some network volumes) that does not give files a stable unique identity; `plugin temp parent is not a regular directory` means that directory is a reparse point, including a cloud-sync placeholder such as an unhydrated OneDrive folder; `plugin temp parent must be writable only by its owner` means its access control list grants write access beyond the current user. For the first two, move the project (and, if the message names the IDE cache path, that cache) to a local NTFS volume outside any cloud-synced folder. The third one is most often a directory created directly on a drive root, which inherits a write grant for a broad group such as `Authenticated Users`; a local NTFS volume does not help there. Move the directory under `%USERPROFILE%`, or drop the inherited grant with `icacls <dir> /inheritance:r /grant %USERNAME%:(OI)(CI)F`.

### No aliases are listed

Run `sf org list --json`. Authenticate aliases if needed, then choose **Reload Orgs**. Entries without an alias are intentionally not shown.

### Source and target are rejected as the same org

Choose aliases that resolve to different org IDs. Two different alias names can still point to one org.

### No components or differences are shown

Confirm that the source user can query metadata and that the component is customizable and non-namespaced. Target-only fields, standard fields, and namespaced components are intentionally omitted. Metadata-identical fields appear as `No change`; by default the review Change filter hides them (only `Add` and `Update` are checked). On the first screen, the **Label** and **API Name** column filters each match a case-insensitive prefix, not any substring; switching **Metadata Type** loads that type's own component list without discarding selections already made for other types.

### A target-object row is disabled

The target object does not exist. Either create the object outside this plugin and compare again, or select its custom `CustomObject` row in the same deployment so the object is created before its fields.

### An alias changed after review

The plugin intentionally aborts before mutation if an alias no longer maps to the reviewed username and org ID. Choose **Back** or **New Comparison** and repeat the comparison.

### Deployment or FLS has failed rows

Open **Show Details** for sanitized Salesforce messages. Check Metadata API permissions, field dependencies, target validation rules, and exact Permission Set/Profile name matches. Missing target containers are skipped; a failing permission component is isolated by container when Salesforce returns a component-specific failure. If every FLS row is `Not Attempted` with the same message, the failure happened earlier, reading the Permission Set/Profile catalog itself, before any container-specific deployment was attempted; the field metadata rows above it are unaffected.

### Cancellation or temporary cleanup reports an error

Choose **Restart** to start a fresh workflow and retry cleanup owned by the current plugin session. If the error persists, close the project after the active Salesforce CLI process has stopped and inspect the IDE system directory before removing anything manually.

## Manual Validation

The live-org smoke test is intentionally separate from automated builds because it mutates Salesforce metadata and FLS. Follow [docs/manual-smoke-test.md](docs/manual-smoke-test.md) only with disposable source and target orgs, record each expected result, and dispose of the test orgs afterward.
