sf-metadata-selector 1.0.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.
Files changed (3) hide show
  1. package/README.md +268 -0
  2. package/index.js +673 -0
  3. package/package.json +59 -0
package/README.md ADDED
@@ -0,0 +1,268 @@
1
+ # Salesforce Metadata Selector CLI
2
+
3
+ > A beautiful, interactive Node.js CLI tool that helps you browse, select, and generate `package.xml` files for Salesforce metadata retrieval.
4
+
5
+ It connects to your Salesforce org via the Salesforce CLI (`sf`), lists all available metadata components, and provides a dark-themed web interface (using Tailwind CSS + Monaco Editor) where you can:
6
+
7
+ - 🔍 **Search and filter** metadata types and components in real-time
8
+ - ✅ **Select/deselect** individual components or entire types
9
+ - 💾 **Save and load** selections as JSON for reuse
10
+ - 🎯 **Use wildcards** (`*`) for entire metadata types
11
+ - 🎨 **Customize API version** dynamically
12
+ - 📊 **Track selection counts** in real-time
13
+ - ⌨️ **Keyboard shortcuts** for common actions
14
+ - 🚫 **Filter namespaced** components (exclude by default, include with flag)
15
+ - 📥 **Download** the final `package.xml` with one click
16
+
17
+ ## Screenshot
18
+ ![sf-metadata-sel](https://raw.githubusercontent.com/mchinnappan100/npmjs-images/main/sf-metadata-selector/sf-metadata-selector-1.png)
19
+
20
+ ## ✨ New Features
21
+
22
+ ### 🔍 Search & Filter
23
+ - **Real-time search** across all metadata types and component names
24
+ - **Highlight matching** components for easy visibility
25
+ - **Auto-hide** non-matching items to focus your selection
26
+
27
+ ### 💾 Save & Load Selections
28
+ - **Export selections** to JSON file for backup or sharing
29
+ - **Import selections** to quickly restore previous work
30
+ - Saves API version and wildcard preferences
31
+
32
+ ### 🎯 Wildcard Support
33
+ - **Toggle wildcard mode** to use `*` for entire metadata types
34
+ - Automatically uses `*` when all components of a type are selected
35
+ - Reduces package.xml size for large deployments
36
+
37
+ ### 📊 Selection Counter
38
+ - **Real-time counter** showing selected vs. total components
39
+ - Helps track your selection progress
40
+ - Updates instantly as you select/deselect
41
+
42
+ ### ⌨️ Keyboard Shortcuts
43
+ - `Ctrl+S` / `Cmd+S` - Download package.xml
44
+ - Easy navigation with keyboard accessibility
45
+
46
+ ### 🎨 Custom API Version
47
+ - **Editable API version** field (defaults to 60.0 or your specified version)
48
+ - Updates package.xml in real-time
49
+ - Supports all Salesforce API versions
50
+
51
+ ### 📈 Progress Indicator
52
+ - Shows percentage progress while fetching metadata
53
+ - Better feedback during long-running operations
54
+
55
+ ## 📋 Features
56
+
57
+ - Lists all metadata types using `sf org list metadata-types`
58
+ - Fetches components per type with `sf org list metadata`
59
+ - **Dark theme** UI with Monaco Editor for real-time `package.xml` preview
60
+ - **Per-type** Select All / Deselect All buttons
61
+ - Global Select All / Deselect All
62
+ - **Namespaced components filtering** (exclude by default, include with `--include-namespaced`)
63
+ - Prevents `ENOBUFS` errors with increased buffer sizes
64
+ - Generates a standalone HTML file
65
+
66
+ ## 🔧 Requirements
67
+
68
+ - **Node.js** ≥ 16
69
+ - **[Salesforce CLI (`sf`)](https://developer.salesforce.com/docs/atlas.en-us.sfdx_setup.meta/sfdx_setup/sfdx_setup_install_cli.htm)** installed and authenticated
70
+ - **npm** to install dependencies
71
+
72
+ ## 📦 Installation
73
+
74
+ ```bash
75
+ npm install -g sf-extract-pkg
76
+
77
+ ```
78
+
79
+ ## 🚀 Usage
80
+
81
+ ### Basic Usage
82
+ ```bash
83
+ # Excludes namespaced components by default
84
+ ./sf-metadata-ui.js -o myOrgAlias
85
+
86
+ # Include namespaced components (managed package items, etc.)
87
+ ./sf-metadata-ui.js -o myOrgAlias --include-namespaced
88
+
89
+ # Specify API version
90
+ ./sf-metadata-ui.js -o myOrgAlias --api-version 65.0
91
+
92
+ # Custom output filename
93
+ ./sf-metadata-ui.js -o myOrgAlias --output my-selector.html
94
+
95
+ # Using username instead of alias
96
+ ./sf-metadata-ui.js -o user@example.com.mySandbox
97
+ ```
98
+
99
+ ### Command Options
100
+ ```
101
+ Options:
102
+ -o, --org <alias|username> Salesforce org alias or username (required)
103
+ --include-namespaced Include components with namespace prefix
104
+ --api-version <version> API version for package.xml (default: 60.0)
105
+ --output <filename> Output HTML filename (default: sf-metadata-selector.html)
106
+ -h, --help Display help for command
107
+ ```
108
+
109
+ ## 📖 How It Works
110
+
111
+ 1. **Connects** to your org using `sf org display`
112
+ 2. **Discovers** metadata types with `sf org list metadata-types`
113
+ 3. **Lists** components for each type with `sf org list metadata` (with progress indicator)
114
+ 4. **Filters** out namespaced components unless `--include-namespaced` is used
115
+ 5. **Generates** a rich HTML interface:
116
+ - **Left panel**: searchable metadata types + checkboxes + controls
117
+ - **Right panel**: Monaco Editor showing real-time `package.xml`
118
+ 6. **Download** button creates `package.xml` ready for deployment
119
+
120
+ ## 🎯 Usage Tips
121
+
122
+ ### Searching
123
+ - Type in the search box to filter by metadata type or component name
124
+ - Matching items are highlighted
125
+ - Non-matching items are hidden
126
+
127
+ ### Saving Selections
128
+ 1. Select your desired components
129
+ 2. Click **💾 Save** button
130
+ 3. Save the JSON file to your computer
131
+ 4. Later, click **📂 Load** to restore selections
132
+
133
+ ### Using Wildcards
134
+ 1. Select all components you want for a metadata type
135
+ 2. Enable **"Use wildcards (*) for all"** checkbox
136
+ 3. Types with all components selected will use `*` instead of listing each member
137
+ 4. Great for CustomObject, ApexClass, etc.
138
+
139
+ ### Keyboard Navigation
140
+ - Use `Tab` to navigate between controls
141
+ - `Space` to toggle checkboxes
142
+ - `Ctrl+S` (or `Cmd+S` on Mac) to download package.xml
143
+
144
+ ## 📄 Example Generated package.xml
145
+
146
+ ### Without Wildcards
147
+ ```xml
148
+ <?xml version="1.0" encoding="UTF-8"?>
149
+ <Package xmlns="http://soap.sforce.com/2006/04/metadata">
150
+ <types>
151
+ <members>Account</members>
152
+ <members>Contact</members>
153
+ <members>CustomObject__c</members>
154
+ <name>CustomObject</name>
155
+ </types>
156
+ <types>
157
+ <members>MyController</members>
158
+ <members>MyUtility</members>
159
+ <name>ApexClass</name>
160
+ </types>
161
+ <version>64.0</version>
162
+ </Package>
163
+ ```
164
+
165
+ ### With Wildcards (all components selected)
166
+ ```xml
167
+ <?xml version="1.0" encoding="UTF-8"?>
168
+ <Package xmlns="http://soap.sforce.com/2006/04/metadata">
169
+ <types>
170
+ <members>*</members>
171
+ <name>CustomObject</name>
172
+ </types>
173
+ <types>
174
+ <members>*</members>
175
+ <name>ApexClass</name>
176
+ </types>
177
+ <version>60.0</version>
178
+ </Package>
179
+ ```
180
+
181
+ ## 🛠️ Troubleshooting
182
+
183
+ ### ENOBUFS Error
184
+ Already mitigated with larger `maxBuffer` (10–20 MB). If still occurring on very large orgs:
185
+ - Use `--include-namespaced false` to reduce component count
186
+ - Split retrieval into multiple package.xml files
187
+ - Increase buffer in the script if needed
188
+
189
+ ### No Components Shown
190
+ - Ensure the org alias/username is authenticated: `sf org list`
191
+ - Check that you have permissions to view metadata
192
+ - Try running with `--include-namespaced` to see if components are namespaced
193
+
194
+ ### package.xml Not Updating
195
+ - Refresh the HTML page
196
+ - Check browser console (F12) for JavaScript errors
197
+ - Ensure Monaco Editor loaded correctly
198
+
199
+ ### Command Not Found
200
+ - Make sure `sf` CLI is installed and in your PATH
201
+ - Run `sf --version` to verify installation
202
+ - See [Salesforce CLI Setup Guide](https://developer.salesforce.com/docs/atlas.en-us.sfdx_setup.meta/sfdx_setup/sfdx_setup_install_cli.htm)
203
+
204
+ ### Search Not Working
205
+ - Clear the search box to see all components
206
+ - Search is case-insensitive
207
+ - Try searching for partial names
208
+
209
+ ### Load Selection Failed
210
+ - Ensure the JSON file is from this tool's Save feature
211
+ - Check that the file isn't corrupted
212
+ - Verify JSON syntax if manually edited
213
+
214
+ ## 🎨 UI Features
215
+
216
+ ### Left Panel
217
+ - **Search box** - Real-time filtering
218
+ - **Selection counter** - Shows X / Total selected
219
+ - **Namespace indicator** - Shows if namespaced components are included/excluded
220
+ - **Action buttons** - Select All, Deselect All, Save, Load
221
+ - **Collapsible sections** - Click to expand/collapse metadata types
222
+ - **Per-type controls** - Select/Deselect All for each type
223
+ - **Highlight on search** - Matching items highlighted in blue
224
+
225
+ ### Right Panel
226
+ - **API Version input** - Change version on the fly
227
+ - **Wildcard toggle** - Enable/disable wildcard mode
228
+ - **Monaco Editor** - Syntax-highlighted XML editor
229
+ - **Download button** - One-click package.xml download
230
+ - **Keyboard hint** - Shows Ctrl+S shortcut
231
+
232
+ ## 🔮 Future Enhancements
233
+
234
+ Potential features for future versions:
235
+ - [ ] **Metadata comparison** between orgs
236
+ - [ ] **Dependency analysis** to show related components
237
+ - [ ] **Multiple package.xml** generation (e.g., split by feature)
238
+ - [ ] **Recent selections** history
239
+ - [ ] **Preset filters** (e.g., "All Apex", "All Objects")
240
+ - [ ] **Diff view** against existing package.xml
241
+ - [ ] **Cloud storage** integration for selections
242
+ - [ ] **Batch operations** across multiple orgs
243
+ - [ ] **Component descriptions** on hover
244
+
245
+
246
+ ### Testing
247
+ ```bash
248
+ # Test with your org
249
+ ./sf-metadata-ui.js -o yourOrgAlias
250
+
251
+ # Test with different options
252
+ ./sf-metadata-ui.js -o yourOrg --include-namespaced --api-version 59.0
253
+ ```
254
+
255
+
256
+ ## 📄 License
257
+
258
+ MIT License - see LICENSE file for details
259
+
260
+ ---
261
+
262
+ **Built with ❤️ for Salesforce developers**
263
+
264
+ ## 🔗 Useful Links
265
+
266
+ - [Salesforce CLI Documentation](https://developer.salesforce.com/docs/atlas.en-us.sfdx_cli_reference.meta/sfdx_cli_reference/cli_reference.htm)
267
+ - [Metadata API Developer Guide](https://developer.salesforce.com/docs/atlas.en-us.api_meta.meta/api_meta/)
268
+ - [Package.xml Reference](https://developer.salesforce.com/docs/atlas.en-us.api_meta.meta/api_meta/meta_deploy.htm)
package/index.js ADDED
@@ -0,0 +1,673 @@
1
+ #!/usr/bin/env node
2
+
3
+ const { program } = require('commander');
4
+ const { execSync } = require('child_process');
5
+ const fs = require('fs');
6
+ const path = require('path');
7
+ const { openResource } = require('open-resource');
8
+
9
+ program
10
+ .name('sf-metadata-ui')
11
+ .description('Generate an interactive HTML page to select metadata and build package.xml')
12
+ .requiredOption('-o, --org <alias|username>', 'Salesforce org alias or username')
13
+ .option('--include-namespaced', 'Include components with namespace prefix (default: exclude them)')
14
+ .option('--api-version <version>', 'API version for package.xml (default: 64.0)', '64.0')
15
+ .option('--output <filename>', 'Output HTML filename (default: sf-metadata-selector.html)', 'sf-metadata-selector.html')
16
+ .parse();
17
+
18
+ const options = program.opts();
19
+ const org = options.org;
20
+ const includeNamespaced = !!options.includeNamespaced;
21
+ const apiVersion = options.apiVersion;
22
+ const outputFile = options.output;
23
+
24
+ async function main() {
25
+ try {
26
+ console.log(`Fetching org info for: ${org} ...`);
27
+
28
+ const orgInfoRaw = execSync(
29
+ `sf org display --target-org "${org}" --json`,
30
+ { encoding: 'utf-8', maxBuffer: 10 * 1024 * 1024 }
31
+ );
32
+ const orgInfo = JSON.parse(orgInfoRaw).result;
33
+
34
+ console.log(`Connected to: ${orgInfo.alias || orgInfo.username} (${orgInfo.instanceUrl})`);
35
+
36
+ console.log('Listing available metadata types...');
37
+ const typesCmd = `sf org list metadata-types --target-org "${org}" --json`;
38
+ const typesRaw = execSync(typesCmd, {
39
+ encoding: 'utf-8',
40
+ maxBuffer: 10 * 1024 * 1024
41
+ });
42
+ const typesData = JSON.parse(typesRaw);
43
+
44
+ let metadataTypes = [];
45
+
46
+ if (typesData.result && Array.isArray(typesData.result)) {
47
+ metadataTypes = typesData.result.map(item => item.metadataName || item.name || item.xmlName);
48
+ } else if (Array.isArray(typesData)) {
49
+ metadataTypes = typesData.map(item => item.metadataName || item.name || item.xmlName);
50
+ } else if (typesData.metadataObjects) {
51
+ metadataTypes = typesData.metadataObjects.map(obj => obj.xmlName);
52
+ } else {
53
+ console.warn('Unexpected format for metadata types. Using fallback list.');
54
+ metadataTypes = getFallbackMetadataTypes();
55
+ }
56
+
57
+ metadataTypes = [...new Set(metadataTypes.filter(t => t && typeof t === 'string'))];
58
+
59
+ console.log(`Found ${metadataTypes.length} metadata types`);
60
+
61
+ const components = [];
62
+ const totalTypes = metadataTypes.length;
63
+ let processedTypes = 0;
64
+
65
+ for (const type of metadataTypes.sort()) {
66
+ try {
67
+ processedTypes++;
68
+ const progress = Math.round((processedTypes / totalTypes) * 100);
69
+ console.log(`[${progress}%] Listing components for ${type} ...`);
70
+
71
+ const listCmd = `sf org list metadata --metadata-type "${type}" --target-org "${org}" --json`;
72
+ const listRaw = execSync(listCmd, {
73
+ encoding: 'utf-8',
74
+ maxBuffer: 20 * 1024 * 1024
75
+ });
76
+ const listData = JSON.parse(listRaw);
77
+
78
+ let items = [];
79
+
80
+ if (listData.result && Array.isArray(listData.result)) {
81
+ items = listData.result;
82
+ } else if (Array.isArray(listData)) {
83
+ items = listData;
84
+ }
85
+
86
+ items.forEach(item => {
87
+ const fullName = item.fullName || item.fullname || item.name;
88
+ if (fullName && (includeNamespaced || !hasNamespace(fullName))) {
89
+ components.push({ type, fullName });
90
+ }
91
+ });
92
+ } catch (err) {
93
+ const msg = err.message || err.toString();
94
+ console.warn(`Skipping ${type} — ${msg.split('\n')[0] || 'error'}`);
95
+ }
96
+ }
97
+
98
+ components.sort((a, b) =>
99
+ a.type.localeCompare(b.type) || a.fullName.localeCompare(b.fullName)
100
+ );
101
+
102
+ console.log(`Found ${components.length} metadata components (namespaced: ${includeNamespaced ? 'included' : 'excluded'})`);
103
+
104
+ const html = generateHtml(components, includeNamespaced, apiVersion, orgInfo);
105
+
106
+ fs.writeFileSync(outputFile, html, 'utf-8');
107
+
108
+ console.log(`\nSuccess! Generated file:`);
109
+ console.log(`→ ${path.resolve(outputFile)}`);
110
+ console.log('Open it in your browser to select components and build package.xml');
111
+
112
+ try {
113
+ openResource(outputFile);
114
+ } catch (err) {
115
+ console.log('Could not auto-open file. Please open it manually.');
116
+ }
117
+
118
+ } catch (err) {
119
+ console.error('Error occurred:');
120
+ console.error(err.message);
121
+ if (err.stderr) console.error('stderr:', err.stderr);
122
+ if (err.stdout) console.error('stdout:', err.stdout);
123
+ process.exit(1);
124
+ }
125
+ }
126
+
127
+ function hasNamespace(name) {
128
+ return /([a-zA-Z0-9]+__)[\w]+/.test(name);
129
+ }
130
+
131
+ function getFallbackMetadataTypes() {
132
+ return [
133
+ 'ApexClass', 'ApexComponent', 'ApexPage', 'ApexTrigger',
134
+ 'AuraDefinitionBundle', 'CustomObject', 'CustomField',
135
+ 'CustomLabel', 'CustomMetadata', 'CustomTab', 'FlexiPage',
136
+ 'LightningComponentBundle', 'Profile', 'PermissionSet',
137
+ 'StaticResource', 'Layout', 'RecordType', 'ValidationRule',
138
+ 'Flow', 'FlowDefinition', 'EmailTemplate', 'ReportType',
139
+ 'Report', 'Dashboard', 'ListView', 'CustomApplication',
140
+ 'CompactLayout', 'QuickAction', 'Territory2Type'
141
+ ];
142
+ }
143
+
144
+ function generateHtml(components, includeNamespaced, apiVersion, orgInfo) {
145
+ const groups = {};
146
+ components.forEach(c => {
147
+ if (!groups[c.type]) groups[c.type] = [];
148
+ groups[c.type].push(c.fullName);
149
+ });
150
+
151
+ const orgName = orgInfo.alias || orgInfo.username;
152
+
153
+ return `<!DOCTYPE html>
154
+ <html lang="en" class="dark">
155
+ <head>
156
+ <meta charset="UTF-8" />
157
+ <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
158
+ <title>Salesforce Metadata Selector - ${orgName}</title>
159
+ <script src="https://cdn.tailwindcss.com"></script>
160
+ <link rel="icon" type="image/x-icon"
161
+ href="https://mohan-chinnappan-n5.github.io/dfv/img/mc_favIcon.ico" />
162
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.44.0/min/vs/loader.min.js"></script>
163
+ <style>
164
+ body { background: #111827; color: #e5e7eb; font-family: ui-sans-serif, system-ui, sans-serif; }
165
+ details summary::-webkit-details-marker { display: none; }
166
+ details summary::before { content: '▶ '; transition: transform 0.2s; display: inline-block; }
167
+ details[open] summary::before { content: '▼ '; transform: rotate(0deg); }
168
+ .highlight {
169
+ background-color: rgba(59, 130, 246, 0.25);
170
+ animation: highlightPulse 0.3s ease-in-out;
171
+ }
172
+ @keyframes highlightPulse {
173
+ 0%, 100% { background-color: rgba(59, 130, 246, 0.25); }
174
+ 50% { background-color: rgba(59, 130, 246, 0.4); }
175
+ }
176
+ #search-box:focus {
177
+ box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.3);
178
+ }
179
+ button {
180
+ transition: all 0.2s ease-in-out;
181
+ }
182
+ button:active {
183
+ transform: scale(0.98);
184
+ }
185
+ .metadata-type-summary {
186
+ transition: background-color 0.2s ease-in-out;
187
+ }
188
+ </style>
189
+ </head>
190
+ <body class="min-h-screen flex flex-col">
191
+ <!-- Enhanced Header -->
192
+ <header class="bg-gradient-to-r from-blue-900 to-blue-800 border-b border-blue-700 px-6 py-4 shadow-lg">
193
+ <div class="max-w-screen-2xl mx-auto">
194
+ <div class="flex items-center justify-between">
195
+ <!-- Title Section -->
196
+ <div class="flex items-center space-x-4">
197
+ <div class="flex items-center justify-center w-10 h-10 bg-blue-600 rounded-lg shadow-md">
198
+ <svg class="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
199
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4m0 5c0 2.21-3.582 4-8 4s-8-1.79-8-4"/>
200
+ </svg>
201
+ </div>
202
+ <div>
203
+ <h1 class="text-xl font-bold text-white">Salesforce Metadata Selector</h1>
204
+ <p class="text-xs text-blue-200 mt-0.5">Generate package.xml for deployment</p>
205
+ </div>
206
+ </div>
207
+
208
+ <!-- Org Info Badge -->
209
+ <div class="flex items-center space-x-3">
210
+ <div class="text-right hidden sm:block">
211
+ <p class="text-xs text-blue-200">Connected Org</p>
212
+ <p style='display:none;' class="text-sm font-semibold text-white">${orgName}</p>
213
+ </div>
214
+ <div class="bg-blue-700 rounded-full px-4 py-2 border border-blue-600">
215
+ <span class="text-xs font-mono text-blue-100">${orgName}</span>
216
+ </div>
217
+ </div>
218
+ </div>
219
+ </div>
220
+ </header>
221
+
222
+ <div class="flex flex-1 gap-6 overflow-hidden p-6">
223
+ <!-- Left Panel: Component Selection -->
224
+ <div class="w-2/5 bg-gray-900 rounded-xl p-5 overflow-auto border border-gray-700 flex flex-col">
225
+ <!-- Search Box -->
226
+ <div class="mb-4 relative">
227
+ <div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
228
+ <svg class="w-5 h-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
229
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
230
+ </svg>
231
+ </div>
232
+ <input
233
+ type="text"
234
+ id="search-box"
235
+ placeholder="Search metadata types or components..."
236
+ class="w-full pl-10 pr-4 py-2.5 bg-gray-800 border border-gray-600 rounded-lg text-sm focus:outline-none focus:border-blue-500 transition-all"
237
+ />
238
+ </div>
239
+
240
+ <!-- Info Bar -->
241
+ <div class="mb-4 bg-gray-800 rounded-lg p-3 border border-gray-700">
242
+ <div class="flex justify-between items-center text-sm">
243
+ <div class="flex items-center space-x-2">
244
+ <svg class="w-4 h-4 text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
245
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
246
+ </svg>
247
+ <span class="text-gray-400">Selection:</span>
248
+ <span class="font-bold text-blue-400" id="selected-count">0</span>
249
+ <span class="text-gray-500">/</span>
250
+ <span class="text-gray-300" id="total-count">${components.length}</span>
251
+ </div>
252
+ <div class="flex items-center space-x-2">
253
+ <svg class="w-4 h-4 ${includeNamespaced ? 'text-green-400' : 'text-orange-400'}" fill="none" stroke="currentColor" viewBox="0 0 24 24">
254
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z"/>
255
+ </svg>
256
+ <span class="text-gray-400">Namespaced:</span>
257
+ <span class="font-medium ${includeNamespaced ? 'text-green-400' : 'text-orange-400'}">
258
+ ${includeNamespaced ? 'Included' : 'Excluded'}
259
+ </span>
260
+ </div>
261
+ </div>
262
+ </div>
263
+
264
+ <!-- Action Buttons -->
265
+ <div class="mb-5 pb-4 border-b border-gray-700">
266
+ <div class="flex flex-col sm:flex-row justify-between items-stretch sm:items-center gap-3">
267
+ <!-- Selection Controls -->
268
+ <div class="flex items-center gap-2">
269
+ <button id="select-all" class="flex-1 sm:flex-none bg-blue-600 hover:bg-blue-700 px-4 py-2 rounded-lg text-sm font-medium transition-colors flex items-center justify-center gap-2">
270
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
271
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
272
+ </svg>
273
+ Select All
274
+ </button>
275
+ <button id="deselect-all" class="flex-1 sm:flex-none bg-red-600 hover:bg-red-700 px-4 py-2 rounded-lg text-sm font-medium transition-colors flex items-center justify-center gap-2">
276
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
277
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"/>
278
+ </svg>
279
+ Deselect All
280
+ </button>
281
+ </div>
282
+
283
+ <!-- File Operations -->
284
+ <div class="flex items-center gap-2">
285
+ <button id="save-selection" class="flex-1 sm:flex-none bg-purple-600 hover:bg-purple-700 px-4 py-2 rounded-lg text-sm font-medium transition-colors flex items-center justify-center gap-2" title="Save selection to JSON">
286
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
287
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7H5a2 2 0 00-2 2v9a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-3m-1 4l-3 3m0 0l-3-3m3 3V4"/>
288
+ </svg>
289
+ Save
290
+ </button>
291
+ <button id="load-selection" class="flex-1 sm:flex-none bg-indigo-600 hover:bg-indigo-700 px-4 py-2 rounded-lg text-sm font-medium transition-colors flex items-center justify-center gap-2" title="Load selection from JSON">
292
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
293
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12"/>
294
+ </svg>
295
+ Load
296
+ </button>
297
+ </div>
298
+ </div>
299
+ </div>
300
+
301
+ <!-- Components List -->
302
+ <div id="components" class="flex-1 overflow-auto"></div>
303
+ </div>
304
+
305
+ <!-- Right Panel: XML Editor -->
306
+ <div class="w-3/5 flex flex-col bg-gray-900 rounded-xl border border-gray-700 overflow-hidden shadow-xl">
307
+ <!-- API Version & Controls Header -->
308
+ <div class="bg-gray-800 border-b border-gray-700 px-5 py-3">
309
+ <div class="flex flex-col sm:flex-row justify-between items-stretch sm:items-center gap-3">
310
+ <!-- API Version -->
311
+ <div class="flex items-center space-x-3">
312
+ <svg class="w-5 h-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
313
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4"/>
314
+ </svg>
315
+ <label class="text-sm font-medium text-gray-300">API Version:</label>
316
+ <input
317
+ type="text"
318
+ id="api-version"
319
+ value="${apiVersion}"
320
+ class="w-24 px-3 py-1.5 bg-gray-900 border border-gray-600 rounded-lg text-sm font-mono focus:outline-none focus:border-blue-500 focus:ring-1 focus:ring-blue-500 transition-all"
321
+ placeholder="64.0"
322
+ />
323
+ </div>
324
+
325
+ <!-- Wildcard Toggle -->
326
+ <div class="flex items-center space-x-2 bg-gray-900 rounded-lg px-4 py-2 border border-gray-700">
327
+ <input type="checkbox" id="use-wildcards" class="w-4 h-4 accent-blue-500 rounded" />
328
+ <label for="use-wildcards" class="text-sm text-gray-300 cursor-pointer select-none flex items-center gap-2">
329
+ <svg class="w-4 h-4 text-yellow-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
330
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11.049 2.927c.3-.921 1.603-.921 1.902 0l1.519 4.674a1 1 0 00.95.69h4.915c.969 0 1.371 1.24.588 1.81l-3.976 2.888a1 1 0 00-.363 1.118l1.518 4.674c.3.922-.755 1.688-1.538 1.118l-3.976-2.888a1 1 0 00-1.176 0l-3.976 2.888c-.783.57-1.838-.197-1.538-1.118l1.518-4.674a1 1 0 00-.363-1.118l-3.976-2.888c-.784-.57-.38-1.81.588-1.81h4.914a1 1 0 00.951-.69l1.519-4.674z"/>
331
+ </svg>
332
+ Use wildcards (*) for all
333
+ </label>
334
+ </div>
335
+ </div>
336
+ </div>
337
+
338
+ <!-- Monaco Editor -->
339
+ <div id="editor" class="flex-1"></div>
340
+
341
+ <!-- Download Section -->
342
+ <div class="bg-gray-800 border-t border-gray-700 px-5 py-4">
343
+ <div class="flex justify-between items-center">
344
+ <div class="flex items-center space-x-4 text-sm text-gray-400">
345
+ <div class="flex items-center gap-2">
346
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
347
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/>
348
+ </svg>
349
+ <span>Keyboard shortcut:</span>
350
+ <kbd class="px-2 py-1 bg-gray-900 rounded text-xs font-mono border border-gray-700">Ctrl+S</kbd>
351
+ </div>
352
+ </div>
353
+ <button id="download" class="bg-green-600 hover:bg-green-700 px-8 py-2.5 rounded-lg font-medium transition-all hover:shadow-lg hover:shadow-green-600/50 flex items-center gap-2">
354
+ <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
355
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"/>
356
+ </svg>
357
+ Download package.xml
358
+ </button>
359
+ </div>
360
+ </div>
361
+ </div>
362
+ </div>
363
+
364
+ <!-- Hidden file input for loading -->
365
+ <input type="file" id="file-input" accept=".json" style="display: none;" />
366
+
367
+ <script>
368
+ const groups = ${JSON.stringify(groups, null, 2)};
369
+ const container = document.getElementById('components');
370
+ const searchBox = document.getElementById('search-box');
371
+ const selectedCountEl = document.getElementById('selected-count');
372
+ const totalCountEl = document.getElementById('total-count');
373
+ const apiVersionInput = document.getElementById('api-version');
374
+ const useWildcardsCheckbox = document.getElementById('use-wildcards');
375
+
376
+ let allDetailsElements = [];
377
+
378
+ function hasNamespace(name) {
379
+ return /([a-zA-Z0-9]+__)\\w+/.test(name);
380
+ }
381
+
382
+ function updateCounts() {
383
+ const checked = document.querySelectorAll('#components input[type="checkbox"]:checked');
384
+ selectedCountEl.textContent = checked.length;
385
+ }
386
+
387
+ function updateXml() {
388
+ if (!editor) return;
389
+
390
+ const apiVersion = apiVersionInput.value || '64.0';
391
+ const useWildcards = useWildcardsCheckbox.checked;
392
+ const checked = document.querySelectorAll('#components input[type="checkbox"]:checked');
393
+ const typeMap = new Map();
394
+
395
+ checked.forEach(chk => {
396
+ const t = chk.dataset.type;
397
+ const m = chk.dataset.fullname;
398
+ if (t && m) {
399
+ if (!typeMap.has(t)) typeMap.set(t, []);
400
+ typeMap.get(t).push(m);
401
+ }
402
+ });
403
+
404
+ let xml = '<?xml version="1.0" encoding="UTF-8"?>\\n';
405
+ xml += '<Package xmlns="http://soap.sforce.com/2006/04/metadata">\\n';
406
+
407
+ [...typeMap.keys()].sort().forEach(t => {
408
+ xml += ' <types>\\n';
409
+
410
+ if (useWildcards) {
411
+ // Check if all components of this type are selected
412
+ const totalForType = groups[t]?.length || 0;
413
+ const selectedForType = typeMap.get(t).length;
414
+
415
+ if (totalForType > 0 && selectedForType === totalForType) {
416
+ xml += ' <members>*</members>\\n';
417
+ } else {
418
+ const members = typeMap.get(t).sort();
419
+ members.forEach(mem => {
420
+ xml += \` <members>\${mem}</members>\\n\`;
421
+ });
422
+ }
423
+ } else {
424
+ const members = typeMap.get(t).sort();
425
+ members.forEach(mem => {
426
+ xml += \` <members>\${mem}</members>\\n\`;
427
+ });
428
+ }
429
+
430
+ xml += \` <name>\${t}</name>\\n </types>\\n\`;
431
+ });
432
+
433
+ xml += \` <version>\${apiVersion}</version>\\n</Package>\`;
434
+
435
+ editor.setValue(xml);
436
+ updateCounts();
437
+ }
438
+
439
+ // Search/Filter functionality
440
+ searchBox.addEventListener('input', (e) => {
441
+ const query = e.target.value.toLowerCase();
442
+
443
+ allDetailsElements.forEach(({ details, type, content }) => {
444
+ const typeMatches = type.toLowerCase().includes(query);
445
+ const labels = content.querySelectorAll('label');
446
+ let hasVisibleMembers = false;
447
+
448
+ labels.forEach(label => {
449
+ const memberName = label.querySelector('span').textContent.toLowerCase();
450
+ const matches = memberName.includes(query);
451
+
452
+ label.style.display = (typeMatches || matches || query === '') ? 'flex' : 'none';
453
+ if (matches || typeMatches || query === '') hasVisibleMembers = true;
454
+ });
455
+
456
+ details.style.display = (hasVisibleMembers || query === '') ? 'block' : 'none';
457
+
458
+ // Highlight matches
459
+ if (query !== '') {
460
+ labels.forEach(label => {
461
+ const span = label.querySelector('span');
462
+ const text = span.textContent;
463
+ if (text.toLowerCase().includes(query)) {
464
+ label.classList.add('highlight');
465
+ } else {
466
+ label.classList.remove('highlight');
467
+ }
468
+ });
469
+ } else {
470
+ labels.forEach(label => label.classList.remove('highlight'));
471
+ }
472
+ });
473
+ });
474
+
475
+ // Save selection to JSON
476
+ document.getElementById('save-selection').addEventListener('click', () => {
477
+ const checked = document.querySelectorAll('#components input[type="checkbox"]:checked');
478
+ const selection = {
479
+ apiVersion: apiVersionInput.value,
480
+ useWildcards: useWildcardsCheckbox.checked,
481
+ selectedComponents: Array.from(checked).map(chk => ({
482
+ type: chk.dataset.type,
483
+ fullName: chk.dataset.fullname
484
+ }))
485
+ };
486
+
487
+ const blob = new Blob([JSON.stringify(selection, null, 2)], { type: 'application/json' });
488
+ const url = URL.createObjectURL(blob);
489
+ const a = document.createElement('a');
490
+ a.href = url;
491
+ a.download = 'metadata-selection.json';
492
+ a.click();
493
+ URL.revokeObjectURL(url);
494
+ });
495
+
496
+ // Load selection from JSON
497
+ document.getElementById('load-selection').addEventListener('click', () => {
498
+ document.getElementById('file-input').click();
499
+ });
500
+
501
+ document.getElementById('file-input').addEventListener('change', (e) => {
502
+ const file = e.target.files[0];
503
+ if (!file) return;
504
+
505
+ const reader = new FileReader();
506
+ reader.onload = (event) => {
507
+ try {
508
+ const selection = JSON.parse(event.target.result);
509
+
510
+ // Deselect all first
511
+ document.querySelectorAll('#components input[type="checkbox"]').forEach(c => c.checked = false);
512
+
513
+ // Apply loaded selection
514
+ if (selection.apiVersion) apiVersionInput.value = selection.apiVersion;
515
+ if (typeof selection.useWildcards === 'boolean') useWildcardsCheckbox.checked = selection.useWildcards;
516
+
517
+ selection.selectedComponents.forEach(item => {
518
+ const checkbox = document.querySelector(
519
+ \`#components input[data-type="\${item.type}"][data-fullname="\${item.fullName}"]\`
520
+ );
521
+ if (checkbox) checkbox.checked = true;
522
+ });
523
+
524
+ updateXml();
525
+ alert('Selection loaded successfully!');
526
+ } catch (err) {
527
+ alert('Error loading selection file: ' + err.message);
528
+ }
529
+ };
530
+ reader.readAsText(file);
531
+ });
532
+
533
+ // Render metadata types with per-type select/deselect
534
+ Object.keys(groups).sort().forEach(type => {
535
+ const members = groups[type];
536
+ if (members.length === 0) return;
537
+
538
+ const count = members.length;
539
+
540
+ const details = document.createElement('details');
541
+ details.className = 'mb-3';
542
+
543
+ const summary = document.createElement('summary');
544
+ summary.className = 'metadata-type-summary bg-gray-800 hover:bg-gray-700 px-4 py-3 rounded-lg cursor-pointer font-medium flex justify-between items-center group';
545
+
546
+ const left = document.createElement('span');
547
+ left.textContent = \`\${type} (\${count})\`;
548
+
549
+ const right = document.createElement('div');
550
+ right.className = 'space-x-2';
551
+ right.onclick = (e) => e.stopPropagation(); // Prevent details toggle
552
+
553
+ const selectAllBtn = document.createElement('button');
554
+ selectAllBtn.textContent = 'Select All';
555
+ selectAllBtn.className = 'text-xs bg-blue-600 hover:bg-blue-500 px-3 py-1 rounded-md transition-colors opacity-0 group-hover:opacity-100';
556
+ selectAllBtn.type = 'button';
557
+
558
+ const deselectAllBtn = document.createElement('button');
559
+ deselectAllBtn.textContent = 'Deselect All';
560
+ deselectAllBtn.className = 'text-xs bg-red-600 hover:bg-red-500 px-3 py-1 rounded-md transition-colors opacity-0 group-hover:opacity-100';
561
+ deselectAllBtn.type = 'button';
562
+
563
+ right.appendChild(selectAllBtn);
564
+ right.appendChild(deselectAllBtn);
565
+
566
+ summary.appendChild(left);
567
+ summary.appendChild(right);
568
+
569
+ const content = document.createElement('div');
570
+ content.className = 'pl-6 pt-2 max-h-80 overflow-auto';
571
+
572
+ members.sort().forEach(name => {
573
+ const label = document.createElement('label');
574
+ label.className = 'flex items-center py-1 hover:bg-gray-800 rounded px-2 cursor-pointer text-sm';
575
+
576
+ const checkbox = document.createElement('input');
577
+ checkbox.type = 'checkbox';
578
+ checkbox.className = 'mr-3 accent-blue-500';
579
+ checkbox.dataset.type = type;
580
+ checkbox.dataset.fullname = name;
581
+
582
+ const span = document.createElement('span');
583
+ span.className = 'font-mono ' + (hasNamespace(name) ? 'text-purple-400' : '');
584
+ span.textContent = name;
585
+
586
+ label.appendChild(checkbox);
587
+ label.appendChild(span);
588
+ content.appendChild(label);
589
+ });
590
+
591
+ details.appendChild(summary);
592
+ details.appendChild(content);
593
+ container.appendChild(details);
594
+
595
+ // Store reference for search
596
+ allDetailsElements.push({ details, type, content });
597
+
598
+ // Per-type select / deselect
599
+ selectAllBtn.addEventListener('click', (e) => {
600
+ e.stopPropagation();
601
+ content.querySelectorAll('input[type="checkbox"]:not([style*="display: none"])').forEach(cb => cb.checked = true);
602
+ updateXml();
603
+ });
604
+
605
+ deselectAllBtn.addEventListener('click', (e) => {
606
+ e.stopPropagation();
607
+ content.querySelectorAll('input[type="checkbox"]').forEach(cb => cb.checked = false);
608
+ updateXml();
609
+ });
610
+ });
611
+
612
+ // Event delegation for checkbox changes
613
+ container.addEventListener('change', (e) => {
614
+ if (e.target.type === 'checkbox') {
615
+ updateXml();
616
+ }
617
+ });
618
+
619
+ // API version and wildcard changes
620
+ apiVersionInput.addEventListener('input', updateXml);
621
+ useWildcardsCheckbox.addEventListener('change', updateXml);
622
+
623
+ // Monaco Editor
624
+ require.config({ paths: { vs: 'https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.44.0/min/vs' }});
625
+ let editor;
626
+
627
+ require(['vs/editor/editor.main'], () => {
628
+ editor = monaco.editor.create(document.getElementById('editor'), {
629
+ value: '<?xml version="1.0" encoding="UTF-8"?>\\n<Package xmlns="http://soap.sforce.com/2006/04/metadata">\\n <!-- Select components → -->\\n <version>${apiVersion}</version>\\n</Package>',
630
+ language: 'xml',
631
+ theme: 'vs-dark',
632
+ automaticLayout: true,
633
+ minimap: { enabled: false },
634
+ fontSize: 13,
635
+ lineNumbers: 'on'
636
+ });
637
+
638
+ // Keyboard shortcut for download (Ctrl+S)
639
+ editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS, () => {
640
+ document.getElementById('download').click();
641
+ });
642
+
643
+ updateXml();
644
+ });
645
+
646
+ // Global Select All / Deselect All
647
+ document.getElementById('select-all').addEventListener('click', () => {
648
+ document.querySelectorAll('#components input[type="checkbox"]:not([style*="display: none"])').forEach(c => c.checked = true);
649
+ updateXml();
650
+ });
651
+
652
+ document.getElementById('deselect-all').addEventListener('click', () => {
653
+ document.querySelectorAll('#components input[type="checkbox"]').forEach(c => c.checked = false);
654
+ updateXml();
655
+ });
656
+
657
+ // Download
658
+ document.getElementById('download').addEventListener('click', () => {
659
+ if (!editor) return;
660
+ const blob = new Blob([editor.getValue()], { type: 'application/xml' });
661
+ const url = URL.createObjectURL(blob);
662
+ const a = document.createElement('a');
663
+ a.href = url;
664
+ a.download = 'package.xml';
665
+ a.click();
666
+ URL.revokeObjectURL(url);
667
+ });
668
+ </script>
669
+ </body>
670
+ </html>`;
671
+ }
672
+
673
+ main();
package/package.json ADDED
@@ -0,0 +1,59 @@
1
+ {
2
+ "name": "sf-metadata-selector",
3
+ "version": "1.0.0",
4
+ "description": "A beautiful, interactive CLI tool for browsing, selecting, and generating package.xml files for Salesforce metadata retrieval with search, save/load, and wildcard support",
5
+ "keywords": [
6
+ "salesforce",
7
+ "sfdx",
8
+ "sf-cli",
9
+ "metadata",
10
+ "package-xml",
11
+ "deployment",
12
+ "devops",
13
+ "cli",
14
+ "interactive",
15
+ "ui",
16
+ "developer-tools"
17
+ ],
18
+ "license": "MIT",
19
+ "author": {
20
+ "name": "Mohan Chinnappan"
21
+ },
22
+ "main": "index.js",
23
+ "bin": {
24
+ "sf-metadata-ui": "./index.js",
25
+ "sf-pkg-builder": "./index.js",
26
+ "sfmd": "./index.js"
27
+ },
28
+ "scripts": {
29
+ "start": "node index.js",
30
+ "test": "echo \"Error: no test specified\" && exit 1"
31
+ },
32
+ "dependencies": {
33
+ "commander": "^11.1.0",
34
+ "open-resource": "^2.0.0"
35
+ },
36
+ "devDependencies": {
37
+ "eslint": "^8.57.0",
38
+ "prettier": "^3.2.5"
39
+ },
40
+ "engines": {
41
+ "node": ">=16.0.0",
42
+ "npm": ">=8.0.0"
43
+ },
44
+ "os": [
45
+ "darwin",
46
+ "linux",
47
+ "win32"
48
+ ],
49
+ "preferGlobal": true,
50
+ "files": [
51
+ "index.js",
52
+ "README.md",
53
+ "LICENSE"
54
+ ],
55
+ "publishConfig": {
56
+ "access": "public",
57
+ "registry": "https://registry.npmjs.org/"
58
+ }
59
+ }