sf-decomposer 6.24.1 → 6.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,13 @@
5
5
 
6
6
  All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
7
7
 
8
+ ## [6.25.0](https://github.com/mcarvin8/sf-decomposer/compare/v6.24.1...v6.25.0) (2026-05-29)
9
+
10
+
11
+ ### Features
12
+
13
+ * **metadata:** bump @salesforce/source-deploy-retrieve ([#467](https://github.com/mcarvin8/sf-decomposer/issues/467)) ([30193f2](https://github.com/mcarvin8/sf-decomposer/commit/30193f2b1a0235e77653d05de28ee76fb0971e92))
14
+
8
15
  ## [6.24.1](https://github.com/mcarvin8/sf-decomposer/compare/v6.24.0...v6.24.1) (2026-05-26)
9
16
 
10
17
 
package/HANDBOOK.md CHANGED
@@ -11,12 +11,12 @@ If you want the underlying option grammar instead of recipes, see the [main READ
11
11
  ## Contents
12
12
 
13
13
  - [Choosing a strategy](#choosing-a-strategy)
14
+ - [Common pitfalls](#common-pitfalls)
14
15
  - [Bots (Agentforce and Einstein)](#bots-agentforce-and-einstein)
15
16
  - [Flexipages (Lightning App / Record / Home pages)](#flexipages-lightning-app--record--home-pages)
16
17
  - [Layouts (page layouts)](#layouts-page-layouts)
17
18
  - [Other deeply-nested types](#other-deeply-nested-types)
18
19
  - [The verification workflow](#the-verification-workflow)
19
- - [Common pitfalls](#common-pitfalls)
20
20
 
21
21
  ## Choosing a strategy
22
22
 
@@ -44,6 +44,26 @@ The full registry lives in [`src/metadata/multiLevelDefaults.ts`](./src/metadata
44
44
 
45
45
  Everything else in this handbook is opt-in.
46
46
 
47
+ ## Common pitfalls
48
+
49
+ **1. "I added two `multiLevel` rules but only one survived."**
50
+ You probably ran two `decompose` invocations back-to-back, one rule each. Don't. The disassembler rewrites `.multi_level.json` on every run, so each call replaces the prior one. Pass every rule for a given component in **one** override entry, in array form.
51
+
52
+ **2. "My `multiLevel` rule is correct but recompose produces a smaller file."**
53
+ On `config-disassembler` Rust ≥ 0.5.0 / Node ≥ 1.3.0 this should not happen — sibling collisions are written to per-element SHA-256 shards and surfaced as a `WARN` (see pitfall #5), not silently overwritten. If you do see a shrunken recomposed file on a current build, treat it as a regression worth capturing as a fixture.
54
+
55
+ **3. "Component-scope override fields look ignored."**
56
+ Component-scope wins over type-scope, but only for fields **the component override explicitly sets**. Fields it leaves out fall through to the type-scope value, then to the run-wide default. If you set `decomposedFormat: "yaml"` on a type and `strategy: "grouped-by-tag"` on the component, the component still gets `decomposedFormat: "yaml"` from the type override.
57
+
58
+ **4. "Reassembly removed my decomposed directory even though I didn't pass `postPurge`."**
59
+ That's by design for `multiLevel` types only. Multi-level recompose has to clean up inner-level directories so the next level can merge their reassembled XML. If you want the decomposed tree preserved for inspection, copy it before running `recompose`.
60
+
61
+ **5. "Decompose succeeded but my decomposed files all have hash names."**
62
+ There are two distinct causes; run the decompose under `RUST_LOG=warn` to tell them apart:
63
+
64
+ - **No `WARN` line.** Your `unique_id_elements` (or the rule's third part) didn't resolve to a non-empty value on those items. Check the source XML for the elements you listed — names are case-sensitive and live at the immediate child level of each repeating item. The plugin only walks one level deep when picking a UID.
65
+ - **A `WARN` line of the form `uniqueIdElements collision: <parentTag> id "X" matched N sibling elements`.** The configured key is too narrow — multiple siblings legitimately share the same value, so the collision detector falls back to per-element SHA-256 hashes for that group rather than overwrite. Add a tiebreaker to `unique_id_elements` (e.g. a compound like `name+recordType`) and re-decompose with `prePurge: true`.
66
+
47
67
  ## Bots (Agentforce and Einstein)
48
68
 
49
69
  **Why this is hard.** Agentforce and Einstein bots both ship as `.bot-meta.xml`, but their internals diverge:
@@ -263,37 +283,17 @@ Always verify a new override before committing it:
263
283
  git stash --include-untracked
264
284
 
265
285
  # 2. Decompose with the new override.
266
- sf decomposer decompose -t bot --config
286
+ sf decomposer decompose -m "bot" --config
267
287
 
268
288
  # 3. Recompose back from the decomposed tree.
269
- sf decomposer recompose -t bot
289
+ sf decomposer recompose -m "bot"
270
290
 
271
291
  # 4. Check the round-trip didn't drift.
272
- sf decomposer verify -t bot --config
292
+ sf decomposer verify -m "bot" --config
273
293
  ```
274
294
 
275
295
  `sf decomposer verify` is non-destructive: it decomposes into a temp dir, recomposes from the temp dir, and compares the result to your committed source. If anything drifts (content, missing file, sibling reorder) it tells you exactly which paths broke. Treat any drift as a blocker — fix the override (or fall back to the previous one) before committing.
276
296
 
277
- ## Common pitfalls
278
-
279
- **1. "I added two `multiLevel` rules but only one survived."**
280
- You probably ran two `decompose` invocations back-to-back, one rule each. Don't. The disassembler rewrites `.multi_level.json` on every run, so each call replaces the prior one. Pass every rule for a given component in **one** override entry, in array form.
281
-
282
- **2. "My `multiLevel` rule is correct but recompose produces a smaller file."**
283
- On `config-disassembler` Rust ≥ 0.5.0 / Node ≥ 1.3.0 this should not happen — sibling collisions are written to per-element SHA-256 shards and surfaced as a `WARN` (see pitfall #5), not silently overwritten. If you do see a shrunken recomposed file on a current build, treat it as a regression worth capturing as a fixture.
284
-
285
- **3. "Component-scope override fields look ignored."**
286
- Component-scope wins over type-scope, but only for fields **the component override explicitly sets**. Fields it leaves out fall through to the type-scope value, then to the run-wide default. If you set `decomposedFormat: "yaml"` on a type and `strategy: "grouped-by-tag"` on the component, the component still gets `decomposedFormat: "yaml"` from the type override.
287
-
288
- **4. "Reassembly removed my decomposed directory even though I didn't pass `postPurge`."**
289
- That's by design for `multiLevel` types only. Multi-level recompose has to clean up inner-level directories so the next level can merge their reassembled XML. If you want the decomposed tree preserved for inspection, copy it before running `recompose`.
290
-
291
- **5. "Decompose succeeded but my decomposed files all have hash names."**
292
- There are two distinct causes; run the decompose under `RUST_LOG=warn` to tell them apart:
293
-
294
- - **No `WARN` line.** Your `unique_id_elements` (or the rule's third part) didn't resolve to a non-empty value on those items. Check the source XML for the elements you listed — names are case-sensitive and live at the immediate child level of each repeating item. The plugin only walks one level deep when picking a UID.
295
- - **A `WARN` line of the form `uniqueIdElements collision: <parentTag> id "X" matched N sibling elements`.** The configured key is too narrow — multiple siblings legitimately share the same value, so the collision detector falls back to per-element SHA-256 hashes for that group rather than overwrite. Add a tiebreaker to `unique_id_elements` (e.g. a compound like `name+recordType`) and re-decompose with `prePurge: true`.
296
-
297
297
  ---
298
298
 
299
299
  When in doubt: write the override, run `verify`, read the diff. Every recipe in this handbook was derived that way.
package/README.md CHANGED
@@ -14,93 +14,117 @@ A Salesforce CLI plugin that **decomposes** large metadata XML files into smalle
14
14
  <details>
15
15
  <summary>Table of Contents</summary>
16
16
 
17
- - [Quick Start](#quick-start)
18
- - [Requirements](#requirements)
19
- - [Why sf-decomposer?](#why-sf-decomposer)
20
- - [Commands](#commands)
21
- - [Manifest-scoped runs](#manifest-scoped-runs)
22
- - [Decompose Strategies](#decompose-strategies)
23
- - [Supported Metadata](#supported-metadata)
24
- - [Troubleshooting](#troubleshooting)
25
- - [Hooks](#hooks)
26
- - [Per-Type & Per-Component Overrides](#per-type--per-component-overrides)
27
- - [Ignore Files](#ignore-files)
28
- - [Issues](#issues)
29
- - [Built With](#built-with)
17
+ - [Setup](#setup)
18
+ - [1. Requirements](#1-requirements)
19
+ - [2. Install the Plugin](#2-install-the-plugin)
20
+ - [3. Configure .forceignore](#3-configure-forceignore)
21
+ - [4. Configure Hooks](#4-configure-hooks-recommended)
22
+ - [Daily Workflow](#daily-workflow)
23
+ - [Reference](#reference)
24
+ - [Commands](#commands)
25
+ - [Decompose Strategies](#decompose-strategies)
26
+ - [Supported Metadata](#supported-metadata)
27
+ - [Manifest-scoped Runs](#manifest-scoped-runs)
28
+ - [Per-Type & Per-Component Overrides](#per-type--per-component-overrides)
29
+ - [Ignore Files](#ignore-files)
30
+ - [Troubleshooting](#troubleshooting)
31
+ - [Built With](#built-with)
30
32
  - [Contributing](#contributing)
31
33
  - [License](#license)
34
+
32
35
  </details>
33
36
 
34
37
  ---
35
38
 
36
- ## Quick Start
37
-
38
- 1. **Install the plugin**
39
+ ## Setup
39
40
 
40
- ```bash
41
- sf plugins install sf-decomposer@x.y.z
42
- ```
41
+ Complete these steps once per project. After setup, see [Daily Workflow](#daily-workflow).
43
42
 
44
- 2. **Retrieve metadata** into your Salesforce DX project (e.g. `sf project retrieve start`).
43
+ ### 1. Requirements
45
44
 
46
- 3. **Decompose** the metadata types you need:
45
+ - [Salesforce CLI](https://developer.salesforce.com/tools/sfdxcli) (`sf`) installed
46
+ - Node.js 20.x or later
47
+ - A Salesforce DX project with `sfdx-project.json` and package directories
47
48
 
48
- ```bash
49
- sf decomposer decompose -m "flow" -m "labels" --postpurge
50
- ```
49
+ **Supported Platforms**
51
50
 
52
- > Combine steps 2 & 3 by configuring the [hooks](#hooks).
51
+ sf-decomposer depends on [config-disassembler-node](https://github.com/mcarvin8/config-disassembler-node), which ships prebuilt native binaries as platform-specific optional npm packages — your package manager installs only the one matching your `os` / `cpu` / `libc`:
53
52
 
54
- 4. **Add decomposed paths to [.forceignore](#forceignore)**
55
- This is **required** so the Salesforce CLI does not treat decomposed files as source. Use the [sample .forceignore](https://raw.githubusercontent.com/mcarvin8/sf-decomposer/main/examples/.forceignore) and adjust extensions for your chosen format (`.xml`, `.json`, `.yaml`, etc.).
53
+ | Platform | Architectures |
54
+ | ----------- | ------------------------------------ |
55
+ | **macOS** | x64 (Intel), arm64 (Apple Silicon) |
56
+ | **Linux** | x64 (gnu + musl), arm64 (gnu + musl) |
57
+ | **Windows** | x64, arm64, ia32 |
56
58
 
57
- 5. **Commit** the decomposed files to version control.
59
+ If your platform or architecture is not listed, open an [issue](https://github.com/mcarvin8/sf-decomposer/issues).
58
60
 
59
- 6. **Before deploy**, recompose and then deploy:
61
+ ### 2. Install the Plugin
60
62
 
61
- ```bash
62
- sf decomposer recompose -m "flow" -m "labels"
63
- sf project deploy start
64
- ```
63
+ ```bash
64
+ sf plugins install sf-decomposer@x.y.z
65
+ ```
65
66
 
66
- Pass `-x manifest/package.xml` to both commands to scope the run to the components in a deploy manifest. Configuring the [hooks](#hooks) automates this step entirely.
67
+ ### 3. Configure .forceignore
67
68
 
68
- ---
69
+ **Required.** The Salesforce CLI must ignore decomposed files or `sf` commands will fail. Configure this before running any decompose or retrieve commands.
69
70
 
70
- ## Requirements
71
+ Copy the [sample .forceignore](https://raw.githubusercontent.com/mcarvin8/sf-decomposer/main/examples/.forceignore) into your project root and adjust the extension patterns for your chosen format (`.xml`, `.json`, `.yaml`, etc.).
71
72
 
72
- - [Salesforce CLI](https://developer.salesforce.com/tools/sfdxcli) (`sf`) installed
73
- - Node.js 20.x or later
74
- - A Salesforce DX project with `sfdx-project.json` and package directories
73
+ ### 4. Configure Hooks (Recommended)
75
74
 
76
- ### Supported Platforms
75
+ Hooks auto-decompose after `sf project retrieve start` and auto-recompose before `sf project deploy start` / `validate` — eliminating manual steps entirely.
77
76
 
78
- sf-decomposer depends on [config-disassembler-node](https://github.com/mcarvin8/config-disassembler-node), which ships prebuilt native binaries as platform-specific optional npm packages — your package manager installs only the one matching your `os` / `cpu` / `libc`:
77
+ Add `.sfdecomposer.config.json` to your project root. Copy and customize one of the sample configs:
79
78
 
80
- | Platform | Architectures |
81
- | ----------- | ------------------------------------ |
82
- | **macOS** | x64 (Intel), arm64 (Apple Silicon) |
83
- | **Linux** | x64 (gnu + musl), arm64 (gnu + musl) |
84
- | **Windows** | x64, arm64, ia32 |
79
+ - [Basic sample](https://raw.githubusercontent.com/mcarvin8/sf-decomposer/main/examples/.sfdecomposer.config.json) — one format and strategy for all types
80
+ - [Sample with overrides](https://raw.githubusercontent.com/mcarvin8/sf-decomposer/main/examples/.sfdecomposer.config.overrides.json) — vary format/strategy per metadata type or component
85
81
 
86
- If your platform or architecture is not listed, please open an [issue](https://github.com/mcarvin8/sf-decomposer/issues).
82
+ | Option | Required | Description |
83
+ | ---------------------------- | ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
84
+ | `metadataSuffixes` | Conditional | Comma-separated metadata suffixes to decompose/recompose. Required unless `manifest` is set; when both are set, run is scoped to the intersection. |
85
+ | `manifest` | Conditional | Path (relative to project root) to a `package.xml` manifest. When set, only listed components are decomposed/recomposed. |
86
+ | `ignorePackageDirectories` | No | Comma-separated package directories to skip. |
87
+ | `prePurge` | No | Remove existing decomposed files before decomposing (default: false). |
88
+ | `postPurge` | No | After decompose: remove originals; after recompose: remove decomposed files (default: false). |
89
+ | `decomposedFormat` | No | `xml`, `json`, `json5`, or `yaml` (default: xml). |
90
+ | `strategy` | No | `unique-id` \| `grouped-by-tag` (default: unique-id). |
91
+ | `decomposeNestedPermissions` | No | With `grouped-by-tag`, set `true` to further decompose permission set and muting permission set object/field permissions. |
92
+ | `overrides` | No | Array of per-type and/or per-component overrides. See [Per-Type & Per-Component Overrides](#per-type--per-component-overrides). |
87
93
 
88
94
  ---
89
95
 
90
- ## Why sf-decomposer?
96
+ ## Daily Workflow
97
+
98
+ **With hooks configured** (recommended):
91
99
 
92
- Salesforce's built-in decomposition is limited. sf-decomposer gives admins and developers more control, flexibility, and better versioning.
100
+ ```
101
+ retrieve → auto-decomposes → review & commit → deploy → auto-recomposes
102
+ ```
103
+
104
+ ```bash
105
+ sf project retrieve start # hooks decompose automatically
106
+ git add . && git commit -m "..." # commit decomposed files
107
+ sf project deploy start # hooks recompose automatically
108
+ ```
93
109
 
94
- - **Broader metadata support** — works with most Metadata API types, not just the subset Salesforce decomposes.
95
- - **Two [strategies](#decompose-strategies)** — `unique-id` (one file per nested element) or `grouped-by-tag` (one file per tag).
96
- - **Multiple formats** — XML, JSON, JSON5, or YAML.
97
- - **Manifest-scoped runs** — pass `-x package.xml` to scope a run to just the components in a deploy manifest, the same way `sf project deploy start -x` does.
98
- - **CI/CD hooks** — auto-decompose after retrieve and auto-recompose before deploy via [.sfdecomposer.config.json](#hooks).
99
- - **Stable ordering and smaller files** — clearer pull requests, fewer merge conflicts.
110
+ **Without hooks** (manual):
111
+
112
+ ```bash
113
+ # After retrieve: decompose
114
+ sf decomposer decompose -m "flow" -m "labels" --postpurge
115
+
116
+ # Before deploy: recompose, then deploy
117
+ sf decomposer recompose -m "flow" -m "labels"
118
+ sf project deploy start
119
+ ```
120
+
121
+ Pass `-x manifest/package.xml` to both `decompose` and `recompose` (and `deploy`) to scope a run to just the components in a deploy manifest.
100
122
 
101
123
  ---
102
124
 
103
- ## Commands
125
+ ## Reference
126
+
127
+ ### Commands
104
128
 
105
129
  | Command | Description |
106
130
  | ------------------------- | ----------------------------------------------------------------------------------- |
@@ -108,7 +132,7 @@ Salesforce's built-in decomposition is limited. sf-decomposer gives admins and d
108
132
  | `sf decomposer recompose` | Recompose decomposed files back into deployment-ready metadata. |
109
133
  | `sf decomposer verify` | Round-trip check: decompose + recompose in a temp directory and diff the originals. |
110
134
 
111
- ### sf decomposer decompose
135
+ #### sf decomposer decompose
112
136
 
113
137
  Decomposes metadata in all local package directories (from `sfdx-project.json`) into smaller files.
114
138
 
@@ -125,13 +149,13 @@ FLAGS
125
149
  --prepurge Remove existing decomposed files before decomposing [default: false]
126
150
  --postpurge Remove original metadata files after decomposing [default: false]
127
151
  -p, --decompose-nested-permissions With grouped-by-tag, further decompose permission set and muting permission set object/field permissions
128
- -c, --config Load per-type and per-component overrides from .sfdecomposer.config.json in the repo root. Only the "overrides" array is consumed. See Per-Type & Per-Component Overrides. [default: false]
152
+ -c, --config Load per-type and per-component overrides from .sfdecomposer.config.json in the repo root. Only the "overrides" array is consumed. [default: false]
129
153
 
130
154
  GLOBAL FLAGS
131
155
  --json Output as JSON.
132
156
  ```
133
157
 
134
- > At least one of `--metadata-type` or `--manifest` is required. When both are supplied, the run is scoped to the intersection of the two.
158
+ > At least one of `--metadata-type` or `--manifest` is required. When both are supplied, the run is scoped to their intersection.
135
159
 
136
160
  **Examples**
137
161
 
@@ -152,7 +176,7 @@ sf decomposer decompose -x "manifest/package.xml" --prepurge
152
176
  sf decomposer decompose -x "manifest/package.xml" -m "permissionset"
153
177
  ```
154
178
 
155
- ### sf decomposer recompose
179
+ #### sf decomposer recompose
156
180
 
157
181
  Recomposes decomposed files into deployment-compatible metadata.
158
182
 
@@ -170,7 +194,7 @@ GLOBAL FLAGS
170
194
  --json Output as JSON.
171
195
  ```
172
196
 
173
- > At least one of `--metadata-type` or `--manifest` is required. When both are supplied, the run is scoped to the intersection of the two.
197
+ > At least one of `--metadata-type` or `--manifest` is required. When both are supplied, the run is scoped to their intersection.
174
198
 
175
199
  **Examples**
176
200
 
@@ -183,7 +207,7 @@ sf decomposer recompose -x "manifest/package.xml"
183
207
  sf project deploy start -x "manifest/package.xml"
184
208
  ```
185
209
 
186
- ### sf decomposer verify
210
+ #### sf decomposer verify
187
211
 
188
212
  Non-destructive round-trip check: copies your package directories into a temp directory under your OS's `tmpdir()`, runs decompose then recompose there, and diffs the rebuilt parents against the originals using **structural XML equality** (sibling and attribute order are ignored). Exits non-zero on any drift; your working tree is never modified.
189
213
 
@@ -198,13 +222,13 @@ FLAGS
198
222
  -i, --ignore-package-directory=<value> Package directory to skip. Repeatable.
199
223
  -s, --strategy=<value> unique-id | grouped-by-tag [default: unique-id]
200
224
  -p, --decompose-nested-permissions With grouped-by-tag, further decompose permission set and muting permission set object/field permissions.
201
- -c, --config Load per-type and per-component overrides from .sfdecomposer.config.json in the repo root, the same as `decompose --config`. [default: false]
225
+ -c, --config Load per-type and per-component overrides from .sfdecomposer.config.json in the repo root. [default: false]
202
226
 
203
227
  GLOBAL FLAGS
204
228
  --json Output as JSON.
205
229
  ```
206
230
 
207
- > At least one of `--metadata-type` or `--manifest` is required. When both are supplied, the run is scoped to the intersection of the two.
231
+ > At least one of `--metadata-type` or `--manifest` is required. When both are supplied, the run is scoped to their intersection.
208
232
 
209
233
  **Examples**
210
234
 
@@ -219,44 +243,11 @@ sf decomposer verify -m "permissionset" -s "grouped-by-tag" -p
219
243
  sf decomposer verify -x "manifest/package.xml" --config
220
244
  ```
221
245
 
222
- Files whose **only** delta is sibling or attribute ordering are reported as informational notices, not drift. Salesforce treats metadata as order-agnostic, so the deploy is safe — the notice just warns that committing the post-recompose output will show a git diff even though the metadata is functionally identical.
223
-
224
- ---
225
-
226
- ## Manifest-scoped runs
227
-
228
- `-x` / `--manifest` is supported by every `sf decomposer` command and accepts the same `package.xml` you pass to `sf project deploy start -x`. Only the listed components are decomposed/recomposed; everything else is left alone.
229
-
230
- - Wildcards (`<members>*</members>`) expand against your local source.
231
- - Folder members (e.g. `MyFolder/MyReport`) resolve by walking the folder.
232
- - Types the plugin does not support (e.g. `CustomObject`, `ApexClass`) are skipped with a warning, so the same manifest can drive both deploys and decomposer runs.
233
- - If both `--metadata-type` and `--manifest` are supplied, the run is scoped to the intersection.
234
-
235
- Example manifest:
236
-
237
- ```xml
238
- <?xml version="1.0" encoding="UTF-8"?>
239
- <Package xmlns="http://soap.sforce.com/2006/04/metadata">
240
- <types>
241
- <members>HR_Admin</members>
242
- <name>PermissionSet</name>
243
- </types>
244
- <types>
245
- <members>Case</members>
246
- <name>Workflow</name>
247
- </types>
248
- <version>58.0</version>
249
- </Package>
250
- ```
251
-
252
- ```bash
253
- sf decomposer recompose -x "manifest/package.xml"
254
- sf project deploy start -x "manifest/package.xml"
255
- ```
246
+ Files whose **only** delta is sibling or attribute ordering are reported as informational notices, not drift. Salesforce treats metadata as order-agnostic, so the deploy is safe — the notice warns that committing the post-recompose output will show a git diff even though the metadata is functionally identical.
256
247
 
257
248
  ---
258
249
 
259
- ## Decompose Strategies
250
+ ### Decompose Strategies
260
251
 
261
252
  > **Tip:** A single decompose run can mix strategies and formats across metadata types — and even across components within the same type — through the `overrides` array (see [Per-Type & Per-Component Overrides](#per-type--per-component-overrides)). Recompose is deterministic from the on-disk sidecar, so any combination round-trips. When switching strategies for an existing component, pass `--prepurge` (or set `prePurge: true`) so leftover files from the previous strategy are removed before the new ones are written.
262
253
 
@@ -306,16 +297,16 @@ permissionsets/
306
297
  └── userPermissions.xml
307
298
  ```
308
299
 
309
- ### Filename safety (unique-id)
300
+ #### Filename safety (unique-id)
310
301
 
311
302
  Two safety nets apply automatically to every shard filename emitted by the **unique-id** strategy. Neither requires configuration:
312
303
 
313
304
  - **Path-segment sanitization (silent).** Characters illegal or reserved on at least one supported filesystem — path separators (`/`, `\`), Windows-reserved chars (`:`, `*`, `?`, `"`, `<`, `>`, `|`), and ASCII control bytes — are replaced with `_`; trailing `.` and spaces are stripped. Sanitized filenames are byte-stable across platforms.
314
305
  - **Sibling-collision fallback (emits `WARN`).** When two or more siblings of the same parent tag would resolve to the same filename (the configured unique-id elements are too narrow, or sanitization folded two distinct values together), every sibling in the colliding group is written to its own per-element SHA-256 shard instead. No row is silently overwritten.
315
306
 
316
- If you see a hash-named shard and want to know whether it came from a collision (vs. simply a missing UID), set `RUST_LOG=warn` and rerun — see [Rust crate logging](#xml-disassemble-output-rust-crate).
307
+ If you see a hash-named shard and want to know whether it came from a collision (vs. a missing UID), set `RUST_LOG=warn` and rerun — see [Rust crate logging](#xml-disassemble-output-rust-crate).
317
308
 
318
- ### Custom Labels Decomposition
309
+ #### Custom Labels Decomposition
319
310
 
320
311
  Custom labels are always decomposed with `unique-id` (grouped-by-tag would be a no-op since every element shares the same tag). Each label is written to its own file:
321
312
 
@@ -326,14 +317,14 @@ labels/
326
317
  └── quoteManual.label-meta.xml
327
318
  ```
328
319
 
329
- ### Additional Permission Set Decomposition
320
+ #### Additional Permission Set Decomposition
330
321
 
331
322
  With **grouped-by-tag**, use `--decompose-nested-permissions` (`-p`) to further decompose permission sets and muting permission sets:
332
323
 
333
324
  - Write each `<objectPermissions>` to its own file under `objectPermissions/`.
334
325
  - Group `<fieldPermissions>` by object under `fieldPermissions/`.
335
326
 
336
- Similar to Salesforces `decomposePermissionSetBeta2`, with more control and format options. Muting permission sets extend the permission set metadata type and support the same decomposition.
327
+ Similar to Salesforce's `decomposePermissionSetBeta2`, with more control and format options. Muting permission sets extend the permission set metadata type and support the same decomposition.
337
328
 
338
329
  ```bash
339
330
  sf decomposer decompose -m "permissionset" -s "grouped-by-tag" -p
@@ -357,7 +348,7 @@ permissionsets/
357
348
  └── Job_Request__c.objectPermissions-meta.xml
358
349
  ```
359
350
 
360
- ### Loyalty Program Setup Decomposition
351
+ #### Loyalty Program Setup Decomposition
361
352
 
362
353
  `loyaltyProgramSetup` is always decomposed with `unique-id`, with a built-in `multiLevel` default that splits `<programProcesses>` into per-process folders containing per-`<parameters>` / per-`<rules>` files.
363
354
 
@@ -386,15 +377,15 @@ loyaltyProgramSetups/
386
377
  └── ...
387
378
  ```
388
379
 
389
- > **Tip:** This three-level layout (`programProcesses` → `parameters`/`rules`) is exactly the multi-level decomposition pattern. The same pattern powers Bots, Flexipages, and Layouts via opt-in `multiLevel` overrides — see the [admin handbook](https://github.com/mcarvin8/sf-decomposer/blob/main/HANDBOOK.md) for those recipes.
380
+ > **Tip:** This three-level layout (`programProcesses` → `parameters`/`rules`) is the multi-level decomposition pattern. The same pattern powers Bots, Flexipages, and Layouts via opt-in `multiLevel` overrides — see the [admin handbook](https://github.com/mcarvin8/sf-decomposer/blob/main/HANDBOOK.md) for those recipes.
390
381
 
391
382
  ---
392
383
 
393
- ## Supported Metadata
384
+ ### Supported Metadata
394
385
 
395
- All parent metadata types from this plugins version of **@salesforce/source-deploy-retrieve** (SDR) are supported, except where noted below.
386
+ All parent metadata types from this plugin's version of **@salesforce/source-deploy-retrieve** (SDR) are supported, except where noted below.
396
387
 
397
- Use the metadata **suffix** for `-m` / `--metadata-type`, as in [SDRs metadataRegistry.json](https://github.com/forcedotcom/source-deploy-retrieve/blob/main/src/registry/metadataRegistry.json), or infer from the file name: `*.{suffix}-meta.xml`.
388
+ Use the metadata **suffix** for `-m` / `--metadata-type`, as in [SDR's metadataRegistry.json](https://github.com/forcedotcom/source-deploy-retrieve/blob/main/src/registry/metadataRegistry.json), or infer from the file name: `*.{suffix}-meta.xml`.
398
389
 
399
390
  | Metadata Type | CLI value | Notes |
400
391
  | --------------------------- | -------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
@@ -409,9 +400,9 @@ Use the metadata **suffix** for `-m` / `--metadata-type`, as in [SDR’s metadat
409
400
  | Marketing App Extension | `marketingappextension` | |
410
401
  | Loyalty Program Setup | `loyaltyProgramSetup` | Only `unique-id` strategy supported; `grouped-by-tag` is overridden. Automatically decomposed further (see [Loyalty Program Setup](#loyalty-program-setup-decomposition)). |
411
402
 
412
- The supported metadata table above provides a quick reference for some types. For a comprehensive breakdown of supported, leaf-only, and unsupported metadata types — including multi-level decomposition patterns, Salesforce native decomposition conflicts, and adapter strategy limitations — see [**METADATA_SUPPORT.md**](./METADATA_SUPPORT.md).
403
+ For a comprehensive breakdown of supported, leaf-only, and unsupported metadata types — including multi-level decomposition patterns, Salesforce native decomposition conflicts, and adapter strategy limitations — see [**METADATA_SUPPORT.md**](./METADATA_SUPPORT.md).
413
404
 
414
- ### Exceptions
405
+ #### Exceptions
415
406
 
416
407
  | Situation | Message |
417
408
  | ---------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
@@ -422,71 +413,40 @@ The supported metadata table above provides a quick reference for some types. Fo
422
413
 
423
414
  ---
424
415
 
425
- ## Troubleshooting
426
-
427
- ### Missing sfdx-project.json
416
+ ### Manifest-scoped Runs
428
417
 
429
- The plugin looks for `sfdx-project.json` from the current directory up to the drive root. If it’s not found:
430
-
431
- ```
432
- Error (1): sfdx-project.json not found in any parent directory.
433
- ```
434
-
435
- ### Package Directories Not Found for Given Metadata Type
418
+ `-x` / `--manifest` is supported by every `sf decomposer` command and accepts the same `package.xml` you pass to `sf project deploy start -x`. Only the listed components are decomposed/recomposed; everything else is left alone.
436
419
 
437
- This plugin relies on the @salesforce/source-deploy-retrieve metadata registry to map each metadata type to its expected directory name.
420
+ - Wildcards (`<members>*</members>`) expand against your local source.
421
+ - Folder members (e.g. `MyFolder/MyReport`) resolve by walking the folder.
422
+ - Types the plugin does not support (e.g. `CustomObject`, `ApexClass`) are skipped with a warning, so the same manifest can drive both deploys and decomposer runs.
423
+ - If both `--metadata-type` and `--manifest` are supplied, the run is scoped to their intersection.
438
424
 
439
- If you provide a metadata type whose corresponding directory does not exist in any of your package directories, the plugin will fail with the following error:
425
+ Example manifest:
440
426
 
427
+ ```xml
428
+ <?xml version="1.0" encoding="UTF-8"?>
429
+ <Package xmlns="http://soap.sforce.com/2006/04/metadata">
430
+ <types>
431
+ <members>HR_Admin</members>
432
+ <name>PermissionSet</name>
433
+ </types>
434
+ <types>
435
+ <members>Case</members>
436
+ <name>Workflow</name>
437
+ </types>
438
+ <version>58.0</version>
439
+ </Package>
441
440
  ```
442
- No directories named ${metadataTypeEntry.directoryName} were found in any package directory.
443
- ```
444
-
445
- For example, if you attempt to decompose Custom Labels but none of your package directories contain a "labels" folder, the plugin will throw this error.
446
441
 
447
- ### XML disassemble output (Rust crate)
448
-
449
- The underlying Rust crate logs through [env_logger](https://docs.rs/env_logger). Set `RUST_LOG` to opt into more verbosity:
450
-
451
- | Level | What it covers |
452
- | ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
453
- | `RUST_LOG=error` | Default. Parse errors and skipped files (leaf-only XML — primitives only, nothing to decompose). |
454
- | `RUST_LOG=warn` | Adds [sibling-collision fallback](#filename-safety-unique-id) signals — one line per colliding group (parent tag, collided id, sibling count). **Recommended in CI** when shipping overrides. |
455
-
456
- Example `WARN` (CustomApplication where four `actionOverrides` siblings shared the action name `View`):
457
-
458
- ```
459
- [2026-05-04T15:21:09Z WARN config_disassembler::xml::builders::build_disassembled_files]
460
- uniqueIdElements collision: <actionOverrides> id "View" matched 4 sibling elements;
461
- falling back to SHA-256 content hashes for the colliding group.
462
- Consider adding more discriminating fields to uniqueIdElements for this metadata type.
442
+ ```bash
443
+ sf decomposer recompose -x "manifest/package.xml"
444
+ sf project deploy start -x "manifest/package.xml"
463
445
  ```
464
446
 
465
447
  ---
466
448
 
467
- ## Hooks
468
-
469
- Put **.sfdecomposer.config.json** in the project root to auto-decompose after `sf project retrieve start` and auto-recompose before `sf project deploy start` / `validate`.
470
-
471
- > Configure [.forceignore](#forceignore) first — the Salesforce CLI must ignore decomposed files or `sf` commands can fail.
472
-
473
- Copy and customize the [sample config](https://raw.githubusercontent.com/mcarvin8/sf-decomposer/main/examples/.sfdecomposer.config.json), or the [sample with overrides](https://raw.githubusercontent.com/mcarvin8/sf-decomposer/main/examples/.sfdecomposer.config.overrides.json) to vary format/strategy per metadata type or component.
474
-
475
- | Option | Required | Description |
476
- | ---------------------------- | ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
477
- | `metadataSuffixes` | Conditional | Comma-separated metadata suffixes to decompose/recompose. Required unless `manifest` is set; when both are set, the run is scoped to the intersection. |
478
- | `manifest` | Conditional | Path (relative to the project root) to a `package.xml` manifest. When set, only the components listed in the manifest are decomposed/recomposed. See `-x` above. |
479
- | `ignorePackageDirectories` | No | Comma-separated package directories to skip. |
480
- | `prePurge` | No | Remove existing decomposed files before decomposing (default: false). |
481
- | `postPurge` | No | After decompose: remove originals; after recompose: remove decomposed files (default: false). |
482
- | `decomposedFormat` | No | xml, json, json5, or yaml (default: xml). |
483
- | `strategy` | No | `unique-id` \| `grouped-by-tag` (default: unique-id). |
484
- | `decomposeNestedPermissions` | No | With grouped-by-tag, set true to further decompose permission set and muting permission set object/field permissions. |
485
- | `overrides` | No | Array of per-type and/or per-component overrides for `decomposedFormat`, `strategy`, `decomposeNestedPermissions`, `prePurge`, and `postPurge`. See [Per-Type & Per-Component Overrides](#per-type--per-component-overrides). |
486
-
487
- ---
488
-
489
- ## Per-Type & Per-Component Overrides
449
+ ### Per-Type & Per-Component Overrides
490
450
 
491
451
  Overrides apply to **decompose only**. Recompose is a deterministic round-trip — it auto-detects format from the on-disk files and does not depend on strategy — so it ignores the `overrides` array.
492
452
 
@@ -516,7 +476,7 @@ By default, a single decompose run uses one format and one strategy across every
516
476
  }
517
477
  ```
518
478
 
519
- ### What can be overridden
479
+ #### What can be overridden
520
480
 
521
481
  | Field | Notes |
522
482
  | ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
@@ -543,7 +503,7 @@ The `<fullName>` part of a component key is the SDR fullName for the component,
543
503
 
544
504
  Component overrides are not a filter. If `--metadata` / `metadataSuffixes` includes `permissionset`, every permission set is still decomposed; the override only changes how the named ones are decomposed. Use `--manifest` / the hook's `manifest` field if you want to scope the run itself to a subset of components.
545
505
 
546
- ### Precedence
506
+ #### Precedence
547
507
 
548
508
  For each component, each option is resolved independently in this order (highest first):
549
509
 
@@ -552,7 +512,7 @@ For each component, each option is resolved independently in this order (highest
552
512
  3. The run-wide value (CLI flag, hook config top-level field, or built-in default).
553
513
  4. Hard plugin rules (e.g. `labels` and `loyaltyProgramSetup` forced to `unique-id`) override all of the above.
554
514
 
555
- ### splitTags grammar
515
+ #### splitTags grammar
556
516
 
557
517
  `splitTags` lets you control how `grouped-by-tag` writes nested arrays for any metadata type. The plugin already applies a known-good default for permission sets when `decomposeNestedPermissions: true` is set; setting `splitTags` directly takes precedence and works for any metadata type.
558
518
 
@@ -589,7 +549,7 @@ Each `<tag>` may appear at most once in a spec. The plugin validates the grammar
589
549
 
590
550
  See the [admin handbook](https://github.com/mcarvin8/sf-decomposer/blob/main/HANDBOOK.md) for additional `splitTags` and `multiLevel` recipes (flows, workflows, layouts, flexipages, bots).
591
551
 
592
- ### multiLevel grammar
552
+ #### multiLevel grammar
593
553
 
594
554
  `multiLevel` enables a second decomposition pass on inner-level files for metadata types whose XML has deeply nested repeatable blocks (e.g. `loyaltyProgramSetup`'s `programProcesses → parameters → ...`, or a Bot's `botVersion → botDialogs → botSteps`). The plugin already applies a known-good default for `loyaltyProgramSetup` when running the `unique-id` strategy; setting `multiLevel` directly takes precedence and works for any metadata type.
595
555
 
@@ -631,7 +591,7 @@ Within one scope, the `(file_pattern, root_to_strip)` pair must be unique across
631
591
  >
632
592
  > **Pass all rules at once.** Sequential single-rule decomposes rewrite `.multi_level.json` and only the last rule survives — bundle every rule for a given component into one override. Use [`sf decomposer verify`](#sf-decomposer-verify) to confirm a new config round-trips before committing it.
633
593
 
634
- ### Opting in from the CLI
594
+ #### Opting in from the CLI
635
595
 
636
596
  CLI users can opt into overrides on `decompose` with the boolean `--config` (`-c`) flag. When set, the plugin reads `.sfdecomposer.config.json` from the repo root (the nearest ancestor directory that contains `sfdx-project.json`):
637
597
 
@@ -649,31 +609,67 @@ The post-retrieve hook automatically picks up `overrides` from `.sfdecomposer.co
649
609
 
650
610
  ---
651
611
 
652
- ## Ignore Files
612
+ ### Ignore Files
653
613
 
654
- ### .forceignore
614
+ #### .forceignore
655
615
 
656
616
  The Salesforce CLI must **ignore** decomposed files and **allow** recomposed files. Use the [sample .forceignore](https://raw.githubusercontent.com/mcarvin8/sf-decomposer/main/examples/.forceignore) and set patterns for the extensions you use (`.xml`, `.json`, `.yaml`, etc.).
657
617
 
658
- ### .sfdecomposerignore
618
+ #### .sfdecomposerignore
659
619
 
660
620
  Optional. In the project root, list paths/patterns to skip when **decomposing** (same syntax as [.gitignore 2.22.1](https://git-scm.com/docs/gitignore)). Ignored files are not recomposed from.
661
621
 
662
- ### .gitignore
622
+ #### .gitignore
663
623
 
664
- Optional. Ignore recomposed metadata so it aren’t committed. See the [sample .gitignore](https://raw.githubusercontent.com/mcarvin8/sf-decomposer/main/examples/.gitignore).
624
+ Optional. Ignore recomposed metadata so it isn't committed. See the [sample .gitignore](https://raw.githubusercontent.com/mcarvin8/sf-decomposer/main/examples/.gitignore).
665
625
 
666
626
  ---
667
627
 
668
- ## Issues
628
+ ### Troubleshooting
629
+
630
+ #### Missing sfdx-project.json
631
+
632
+ The plugin looks for `sfdx-project.json` from the current directory up to the drive root. If it's not found:
633
+
634
+ ```
635
+ Error (1): sfdx-project.json not found in any parent directory.
636
+ ```
637
+
638
+ #### Package Directories Not Found for Given Metadata Type
639
+
640
+ This plugin relies on the @salesforce/source-deploy-retrieve metadata registry to map each metadata type to its expected directory name.
641
+
642
+ If you provide a metadata type whose corresponding directory does not exist in any of your package directories, the plugin will fail with:
643
+
644
+ ```
645
+ No directories named ${metadataTypeEntry.directoryName} were found in any package directory.
646
+ ```
647
+
648
+ For example, if you attempt to decompose Custom Labels but none of your package directories contain a "labels" folder, the plugin will throw this error.
649
+
650
+ #### XML disassemble output (Rust crate)
651
+
652
+ The underlying Rust crate logs through [env_logger](https://docs.rs/env_logger). Set `RUST_LOG` to opt into more verbosity:
669
653
 
670
- Bugs and feature requests: open an [issue](https://github.com/mcarvin8/sf-decomposer/issues).
654
+ | Level | What it covers |
655
+ | ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
656
+ | `RUST_LOG=error` | Default. Parse errors and skipped files (leaf-only XML — primitives only, nothing to decompose). |
657
+ | `RUST_LOG=warn` | Adds [sibling-collision fallback](#filename-safety-unique-id) signals — one line per colliding group (parent tag, collided id, sibling count). **Recommended in CI** when shipping overrides. |
658
+
659
+ Example `WARN` (CustomApplication where four `actionOverrides` siblings shared the action name `View`):
660
+
661
+ ```
662
+ [2026-05-04T15:21:09Z WARN config_disassembler::xml::builders::build_disassembled_files]
663
+ uniqueIdElements collision: <actionOverrides> id "View" matched 4 sibling elements;
664
+ falling back to SHA-256 content hashes for the colliding group.
665
+ Consider adding more discriminating fields to uniqueIdElements for this metadata type.
666
+ ```
671
667
 
672
668
  ---
673
669
 
674
- ## Built With
670
+ ### Built With
675
671
 
676
- - [config-disassembler-node](https://github.com/mcarvin8/config-disassembler-node) – Disassemble XML (and other config formats) into smaller, manageable files and reassemble when needed. Node.js + Rust (NAPI-RS). See [Requirements](#requirements).
672
+ - [config-disassembler-node](https://github.com/mcarvin8/config-disassembler-node) – Disassemble XML (and other config formats) into smaller, manageable files and reassemble when needed. Node.js + Rust (NAPI-RS).
677
673
  - [@salesforce/source-deploy-retrieve](https://github.com/forcedotcom/source-deploy-retrieve) – JavaScript toolkit for working with Salesforce metadata.
678
674
 
679
675
  ---
@@ -342,5 +342,5 @@
342
342
  ]
343
343
  }
344
344
  },
345
- "version": "6.24.1"
345
+ "version": "6.25.0"
346
346
  }
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "sf-decomposer",
3
3
  "description": "Split large Salesforce metadata files into version-control-friendly pieces and rebuild deployment-ready files.",
4
- "version": "6.24.1",
4
+ "version": "6.25.0",
5
5
  "dependencies": {
6
6
  "@oclif/core": "4.11.4",
7
7
  "@salesforce/core": "8.26.3",
8
8
  "@salesforce/sf-plugins-core": "12.2.6",
9
- "@salesforce/source-deploy-retrieve": "12.35.9",
9
+ "@salesforce/source-deploy-retrieve": "12.36.0",
10
10
  "config-disassembler": "2.3.0"
11
11
  },
12
12
  "devDependencies": {