meta-compare 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.
package/README.md ADDED
@@ -0,0 +1,38 @@
1
+ # Salesforce Metadata Comparer (meta-compare)
2
+
3
+ > A CLI tool to compare Salesforce metadata between two orgs, view KPIs, and export CSV results.
4
+
5
+ ---
6
+
7
+ ## Features
8
+
9
+ - Authenticate multiple Salesforce orgs using CLI aliases or usernames.
10
+ - Fetch metadata types like:
11
+ - Apex Classes
12
+ - Triggers
13
+ - Custom Objects
14
+ - Flows
15
+ - Page Layouts
16
+ - Profiles
17
+ - Permission Sets
18
+ - Validation Rules
19
+ - Compare metadata between two orgs:
20
+ - Identify common components
21
+ - List components only in Source Org
22
+ - List components only in Target Org
23
+ - Interactive data tables with:
24
+ - Search
25
+ - Column sorting
26
+ - CSV export
27
+ - KPI summary with large-font counts.
28
+ - Deploy metadata changes directly to the target org.
29
+ - Dark theme UI for comfortable viewing.
30
+
31
+ ---
32
+
33
+ ## Installation
34
+
35
+ Install globally via npm:
36
+
37
+ ```bash
38
+ npm install -g meta-compare
package/auth.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "mchinnappan@salesforce.com.devmerge": {
3
+ "alias": "mchinnappan@salesforce.com.devmerge",
4
+ "accessToken": "00DbZ000002LcM5!AQEAQKoZUelI3v4urYpbLEhg4JQNWdFt7mFxqYeH7GHXrP76MavYtUM9uAO.BuWJ1vm3r8P3W.gkk1WxOJxcitffr3aEVdBl",
5
+ "instanceUrl": "https://agility-computing-4466--devmerge.sandbox.my.salesforce.com",
6
+ "apiVersion": "65.0"
7
+ },
8
+ "mchinnappan@salesforce.com.datatest": {
9
+ "alias": "mchinnappan@salesforce.com.datatest",
10
+ "accessToken": "00DAs00000ozITC!AQEAQNd.Hdgt7L2zCmeHRyXtR6F8xYGoGlU_qwbX.26DS1Mq9t4lBQoOBOQ8Nsss1uVrFL7vYV.vlNZ6OYZ5jsPqHCBXKp4j",
11
+ "instanceUrl": "https://agility-computing-4466--datatest.sandbox.my.salesforce.com",
12
+ "apiVersion": "64.0"
13
+ },
14
+ "mchinnappan@salesforce.com.p2o": {
15
+ "alias": "mchinnappan@salesforce.com.p2o",
16
+ "accessToken": "00DAq000009MwBR!AQEAQAVnw0xYroZjiEHtpW_i7VQTUQ0P8yLa1DQfLeSIXFGH4rB4imspjLWNNHx1xtb9Sn7m3r8kw2hwTBY0QAua.Xy1gX9m",
17
+ "instanceUrl": "https://agility-computing-4466--p2o.sandbox.my.salesforce.com",
18
+ "apiVersion": "65.0"
19
+ },
20
+ "mchinnappan@salesforce.com.qa": {
21
+ "alias": "mchinnappan@salesforce.com.qa",
22
+ "accessToken": "00DAq000009PEFV!AQEAQAkq1Wbn3aqpVaJ2F8P0o6qlCRY9eaHT8YvjF.XzmSDCR5pur9Lo3kFQrRozfErNYr6VDS.QAT1T7f_QaGkaNCHVasBz",
23
+ "instanceUrl": "https://agility-computing-4466--qa.sandbox.my.salesforce.com",
24
+ "apiVersion": "65.0"
25
+ }
26
+ }
package/doc.html ADDED
@@ -0,0 +1,185 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en" class="dark">
3
+
4
+ <head>
5
+ <meta charset="UTF-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <title>Salesforce Metadata Comparer</title>
8
+
9
+ <!-- Tailwind CSS CDN -->
10
+ <script src="https://cdn.tailwindcss.com"></script>
11
+ <script>
12
+ tailwind.config = {
13
+ darkMode: 'class',
14
+ theme: {
15
+ extend: {
16
+ colors: {
17
+ 'sf-blue': '#0176d3',
18
+ 'sf-dark-blue': '#014486',
19
+ 'sf-green': '#10b981',
20
+ 'sf-red': '#ef4444'
21
+ }
22
+ }
23
+ }
24
+ }
25
+ </script>
26
+
27
+ <!-- DataTables CSS -->
28
+ <link rel="stylesheet" href="https://cdn.datatables.net/1.13.8/css/jquery.dataTables.min.css">
29
+ <link rel="stylesheet" href="https://cdn.datatables.net/responsive/2.5.0/css/responsive.dataTables.min.css">
30
+
31
+ <style>
32
+ body {
33
+ background-color: #111827;
34
+ color: #e5e7eb;
35
+ }
36
+
37
+ header,
38
+ footer {
39
+ background-color: #1f2937;
40
+ }
41
+
42
+ table.dataTable tbody tr {
43
+ background-color: #1f2937;
44
+ }
45
+
46
+ table.dataTable tbody tr.odd {
47
+ background-color: #111827;
48
+ }
49
+
50
+ table.dataTable thead {
51
+ background-color: #374151;
52
+ color: #f3f4f6;
53
+ }
54
+ </style>
55
+ </head>
56
+
57
+ <body class="flex flex-col min-h-screen">
58
+
59
+ <!-- Header -->
60
+ <header class="sticky top-0 z-50 p-4 border-b border-gray-700 flex justify-between items-center">
61
+ <h1 class="text-2xl font-bold text-sf-blue flex items-center gap-2">
62
+ <svg class="w-6 h-6 text-sf-blue" fill="currentColor" viewBox="0 0 24 24">
63
+ <path d="M12 2L2 7v10l10 5 10-5V7L12 2zm0 2.18L19.82 8 12 11.82 4.18 8 12 4.18zM4 9.82l7 3.5v7.36l-7-3.5V9.82zm16 0v7.36l-7 3.5v-7.36l7-3.5z" />
64
+ </svg>
65
+ Salesforce Metadata Comparer
66
+ </h1>
67
+ <div>
68
+ <button id="refresh-btn" class="px-4 py-2 bg-sf-blue hover:bg-sf-dark-blue text-white font-medium rounded-md">🔄 Refresh</button>
69
+ </div>
70
+ </header>
71
+
72
+ <!-- Main Content -->
73
+ <main class="flex-1 p-6 space-y-8 overflow-auto">
74
+
75
+ <!-- KPI Section -->
76
+ <div class="grid grid-cols-3 gap-6 text-center">
77
+ <div class="bg-gray-800 rounded-lg p-6 border border-gray-700">
78
+ <div class="text-5xl font-extrabold text-sf-blue" id="kpi-common">0</div>
79
+ <div class="text-gray-300 mt-2">Common</div>
80
+ </div>
81
+ <div class="bg-gray-800 rounded-lg p-6 border border-gray-700">
82
+ <div class="text-5xl font-extrabold text-sf-red" id="kpi-org1">0</div>
83
+ <div class="text-gray-300 mt-2">Only in Source Org</div>
84
+ </div>
85
+ <div class="bg-gray-800 rounded-lg p-6 border border-gray-700">
86
+ <div class="text-5xl font-extrabold text-sf-green" id="kpi-org2">0</div>
87
+ <div class="text-gray-300 mt-2">Only in Target Org</div>
88
+ </div>
89
+ </div>
90
+
91
+ <!-- Metadata Tables -->
92
+ <div class="grid grid-cols-2 gap-6">
93
+ <!-- Only in Source Org -->
94
+ <div>
95
+ <div class="flex justify-between items-center mb-2">
96
+ <h2 class="text-xl font-semibold">Only in Source Org</h2>
97
+ <button id="download-org1" class="px-3 py-1 bg-blue-600 hover:bg-blue-700 text-white text-sm rounded-md">📥 CSV</button>
98
+ </div>
99
+ <table id="table-org1" class="display stripe hover w-full">
100
+ <thead><tr><th>Metadata Name</th></tr></thead>
101
+ <tbody></tbody>
102
+ </table>
103
+ </div>
104
+
105
+ <!-- Only in Target Org -->
106
+ <div>
107
+ <div class="flex justify-between items-center mb-2">
108
+ <h2 class="text-xl font-semibold">Only in Target Org</h2>
109
+ <button id="download-org2" class="px-3 py-1 bg-blue-600 hover:bg-blue-700 text-white text-sm rounded-md">📥 CSV</button>
110
+ </div>
111
+ <table id="table-org2" class="display stripe hover w-full">
112
+ <thead><tr><th>Metadata Name</th></tr></thead>
113
+ <tbody></tbody>
114
+ </table>
115
+ </div>
116
+ </div>
117
+
118
+ <!-- Common Metadata -->
119
+ <div>
120
+ <div class="flex justify-between items-center mb-2">
121
+ <h2 class="text-xl font-semibold">Common Metadata</h2>
122
+ <button id="download-common" class="px-3 py-1 bg-blue-600 hover:bg-blue-700 text-white text-sm rounded-md">📥 CSV</button>
123
+ </div>
124
+ <table id="table-common" class="display stripe hover w-full">
125
+ <thead><tr><th>Metadata Name</th></tr></thead>
126
+ <tbody></tbody>
127
+ </table>
128
+ </div>
129
+
130
+ </main>
131
+
132
+ <!-- Footer -->
133
+ <footer class="sticky bottom-0 z-50 p-4 border-t border-gray-700 text-center bg-gray-800">
134
+ <span class="text-gray-400">© 2025 Salesforce Metadata Comparer</span>
135
+ </footer>
136
+
137
+ <!-- Scripts -->
138
+ <script src="https://code.jquery.com/jquery-3.7.0.min.js"></script>
139
+ <script src="https://cdn.datatables.net/1.13.8/js/jquery.dataTables.min.js"></script>
140
+ <script src="https://cdn.datatables.net/responsive/2.5.0/js/dataTables.responsive.min.js"></script>
141
+ <script>
142
+ $(document).ready(function () {
143
+ // Initialize DataTables
144
+ const tableOrg1 = $('#table-org1').DataTable({ responsive: true });
145
+ const tableOrg2 = $('#table-org2').DataTable({ responsive: true });
146
+ const tableCommon = $('#table-common').DataTable({ responsive: true });
147
+
148
+ // CSV download function
149
+ function downloadCSV(filename, rows) {
150
+ const csvContent = "data:text/csv;charset=utf-8,"
151
+ + rows.map(e => e.join(",")).join("\n");
152
+ const encodedUri = encodeURI(csvContent);
153
+ const link = document.createElement("a");
154
+ link.setAttribute("href", encodedUri);
155
+ link.setAttribute("download", filename);
156
+ document.body.appendChild(link);
157
+ link.click();
158
+ document.body.removeChild(link);
159
+ }
160
+
161
+ // CSV buttons
162
+ $('#download-org1').on('click', () => {
163
+ const rows = tableOrg1.rows({ search: 'applied' }).data().toArray().map(r => [r[0]]);
164
+ downloadCSV('only-in-source.csv', rows);
165
+ });
166
+
167
+ $('#download-org2').on('click', () => {
168
+ const rows = tableOrg2.rows({ search: 'applied' }).data().toArray().map(r => [r[0]]);
169
+ downloadCSV('only-in-target.csv', rows);
170
+ });
171
+
172
+ $('#download-common').on('click', () => {
173
+ const rows = tableCommon.rows({ search: 'applied' }).data().toArray().map(r => [r[0]]);
174
+ downloadCSV('common-metadata.csv', rows);
175
+ });
176
+
177
+ // Refresh button placeholder
178
+ $('#refresh-btn').on('click', () => {
179
+ location.reload();
180
+ });
181
+ });
182
+ </script>
183
+ </body>
184
+
185
+ </html>
package/main.js ADDED
@@ -0,0 +1,60 @@
1
+ #!/usr/bin/env node
2
+ const { program } = require('commander');
3
+ const path = require('path');
4
+ const fs = require('fs');
5
+ const {
6
+ authenticate,
7
+ fetchMetadata,
8
+ compareMetadata,
9
+ deployMetadata
10
+ } = require('./renderer');
11
+
12
+ program
13
+ .name('sf-metadata-cli')
14
+ .description('CLI to compare Salesforce metadata between two orgs')
15
+ .version('1.0.0');
16
+
17
+ program
18
+ .command('compare')
19
+ .description('Compare metadata between two orgs')
20
+ .requiredOption('-s, --source <alias>', 'Source org alias or username')
21
+ .requiredOption('-t, --target <alias>', 'Target org alias or username')
22
+ .requiredOption('-m, --metadata <type>', 'Metadata type to compare (ApexClass, ApexTrigger, CustomObject, etc.)')
23
+ .action(async (options) => {
24
+ try {
25
+ console.log(`🔹 Fetching auth for Source Org: ${options.source}`);
26
+ const org1Auth = await authenticate(options.source);
27
+
28
+ console.log(`🔹 Fetching auth for Target Org: ${options.target}`);
29
+ const org2Auth = await authenticate(options.target);
30
+
31
+ console.log(`🔹 Comparing metadata type: ${options.metadata}`);
32
+ await compareMetadata(org1Auth, org2Auth, options.metadata);
33
+
34
+ } catch (err) {
35
+ console.error(`❌ Error: ${err.message}`);
36
+ process.exit(1);
37
+ }
38
+ });
39
+
40
+ /*
41
+ program
42
+ .command('deploy')
43
+ .description('Deploy metadata folder to target org')
44
+ .requiredOption('-t, --target <alias>', 'Target org alias or username')
45
+ .requiredOption('-d, --dir <folder>', 'Folder containing metadata to deploy')
46
+ .action(async (options) => {
47
+ try {
48
+ console.log(`🔹 Fetching auth for Target Org: ${options.target}`);
49
+ const targetAuth = await authenticate(options.target);
50
+
51
+ console.log(`🔹 Deploying folder: ${options.dir}`);
52
+ await deployMetadata(targetAuth, options.dir);
53
+ } catch (err) {
54
+ console.error(`❌ Error: ${err.message}`);
55
+ process.exit(1);
56
+ }
57
+ });
58
+ */
59
+
60
+ program.parse(process.argv);
package/output.json ADDED
@@ -0,0 +1,109 @@
1
+ ⚖️ Comparing metadata type: ApexClass between mchinnappan@salesforce.com.devmerge and mchinnappan@salesforce.com.datatest...
2
+
3
+ 📊 Comparison Results: ApexClass
4
+ ✅ Common: 201
5
+ ❌ Only in mchinnappan@salesforce.com.devmerge: 376
6
+ ❌ Only in mchinnappan@salesforce.com.datatest: 0
7
+ → Only in mchinnappan@salesforce.com.devmerge: [
8
+ 'omnistudio__ObjectDocumentCreationDocxService',
9
+ 'omnistudio__FlexCardCompilerTest',
10
+ 'omnistudio__DocTemplateListControllerTest',
11
+ 'omnistudio__DefaultUserCustomLabelsImplementation',
12
+ 'omnistudio__DocuSignBatch',
13
+ 'omnistudio__MapUtilityTest',
14
+ 'omnistudio__ElementTriggerHandlerTest',
15
+ 'omnistudio__OmniStudioPostUninstallClassTest',
16
+ 'omnistudio__DefaultOmniScriptNamedCredentialCallout',
17
+ 'omnistudio__FlexRuntimeTest',
18
+ 'omnistudio__InvokeResource',
19
+ 'IPDocumentGeneratorInvocable',
20
+ 'omnistudio__OmniLanguageDynamicPicklist',
21
+ 'omnistudio__OrgCacheManagerInterface',
22
+ 'omnistudio__XmlToJson',
23
+ 'omnistudio__VlocityDocsUtils',
24
+ 'omnistudio__Logger',
25
+ 'omnistudio__MetadataQueryWithoutSharing',
26
+ 'omnistudio__ObjectDocumentCreationDocxServiceTest',
27
+ 'omnistudio__DocumentServiceClient',
28
+ 'omnistudio__NamespaceUtilitiesTest',
29
+ 'omnistudio__DRUtilities',
30
+ 'OrderAndOrderProductStatusCheckHelper',
31
+ 'omnistudio__JSONMessage',
32
+ 'omnistudio__BusinessProcessTriggerHandlerTest',
33
+ 'omnistudio__DefaultOmniScriptEditBlock',
34
+ 'omnistudio__OmniUiCardTriggerHandler',
35
+ 'omnistudio__AutoCustomLabelReferenceTestDocgen',
36
+ 'omnistudio__ScaleCacheService',
37
+ 'omnistudio__InvokeService',
38
+ 'omnistudio__ClmMockHttpPdfResponse',
39
+ 'omnistudio__OmniScriptUtil',
40
+ 'omnistudio__PricingMatrixCalculationServiceTest',
41
+ 'omnistudio__OmniTypeDynamicPicklistTest',
42
+ 'OrderIdentifierService',
43
+ 'omnistudio__VlocityTrackingServiceFieldMappingsTest',
44
+ 'BellOMSAsyncHelper',
45
+ 'omnistudio__LabelUtilityService',
46
+ 'CaseTriggerTest',
47
+ 'omnistudio__VlocityErrorLoggingServiceTest',
48
+ 'omnistudio__MatchingKeyService',
49
+ 'omnistudio__IntegrationProcedureService',
50
+ 'omnistudio__OmniScriptInstanceController',
51
+ 'omnistudio__CurrencyCode',
52
+ 'omnistudio__DRDocTemplateIntegrationUtilityService',
53
+ 'omnistudio__ObjectUtilitiesTest',
54
+ 'omnistudio__VlocityOpenInterface2',
55
+ 'omnistudio__PDFTronUtilTest',
56
+ 'omnistudio__DRDataPackOUIOpenImplementation',
57
+ 'omnistudio__PDFTRonUtil',
58
+ 'omnistudio__LWCUtilities',
59
+ 'omnistudio__JSONDownloadController',
60
+ 'omnistudio__AutoCustomLabelReferenceTestResources',
61
+ 'OrderUpdateToAbandonedBatchTest',
62
+ 'omnistudio__GuestUserRESTController',
63
+ 'omnistudio__MockDocuSignCalloutResponse',
64
+ 'omnistudio__PersonAccountSetting',
65
+ 'omnistudio__PlatformDocuSignIntegrationService',
66
+ 'omnistudio__StoreResponses',
67
+ 'omnistudio__SessionIdHandler',
68
+ 'omnistudio__SideBySideNavigationControllerTest',
69
+ 'omnistudio__DRRestResource',
70
+ 'omnistudio__VlocityDocsUtilsTest',
71
+ 'omnistudio__DRBundleTriggerHandler',
72
+ 'omnistudio__DefaultOmniScriptSObjectPicklist',
73
+ 'omnistudio__OmniNamedCredentialMockResponse',
74
+ 'omnistudio__DocgenPostInstallClassTest',
75
+ 'omnistudio__GuestUserUtilities',
76
+ 'omnistudio__OrgCacheManagerTest',
77
+ 'omnistudio__FetchStaticResourceUrlTest',
78
+ 'omnistudio__DefaultOmniScriptEditBlockTest',
79
+ 'omnistudio__PricingMatrixCalculationService',
80
+ 'omnistudio__PersonAccountSettingTest',
81
+ 'omnistudio__IsFoundationPkg',
82
+ 'omnistudio__VlocityTrackingService',
83
+ 'omnistudio__ObjectDescriberV2',
84
+ 'omnistudio__DocumentTemplateSectionModel',
85
+ 'omnistudio__DRDataPackMappingsUtility',
86
+ 'omnistudio__SoqlQueryBuilderTest',
87
+ 'omnistudio__DocgenOpenInterfaceSharingWrapperTest',
88
+ 'omnistudio__DRProcessorTest',
89
+ 'omnistudio__RTEPropSetManagerTest',
90
+ 'omnistudio__DefaultDocuSignOmniScriptIntegration',
91
+ 'omnistudio__DocumentTemplateDisplayControllerTest',
92
+ 'omnistudio__CreateEleTypeToHTMLTemplateListTest',
93
+ 'omnistudio__SoqlConditionBuilder',
94
+ 'omnistudio__DefaultOmniScriptSendEmailTest',
95
+ 'omnistudio__TokenMappingComponentController',
96
+ 'NewOrderControllerTest',
97
+ 'omnistudio__PlatformSecurityCheckerTest',
98
+ 'omnistudio__TokenExtractionUtility',
99
+ 'omnistudio__CreateEleTypeToHTMLTemplateList',
100
+ 'omnistudio__PlatformCustomSettingsUtilities',
101
+ 'omnistudio__AccessTokenController',
102
+ 'omnistudio__LegalBannerControllerTest',
103
+ 'omnistudio__NewportTestUtils',
104
+ 'TAGenerateOrderNumberAI',
105
+ 'omnistudio__AccessTokenControllerTest',
106
+ 'omnistudio__DocumentServiceGatewayTest',
107
+ 'OrderStatusResponseHelper',
108
+ ... 276 more items
109
+ ]
package/package.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "meta-compare",
3
+ "version": "1.0.0",
4
+ "description": "A CLI tool to compare Salesforce metadata between two orgs, view KPIs, and export CSV results.",
5
+ "main": "main.js",
6
+ "bin": {
7
+ "meta-compare": "./main.js"
8
+ },
9
+ "scripts": {
10
+ "test": "echo \"Error: no test specified\" && exit 1"
11
+ },
12
+ "keywords": [
13
+ "salesforce",
14
+ "metadata",
15
+ "compare",
16
+ "cli",
17
+ "sfcli",
18
+ "org"
19
+ ],
20
+ "author": "Mohan Chinnappan",
21
+ "license": "MIT",
22
+ "dependencies": {
23
+ "commander": "^14.0.1"
24
+ },
25
+ "engines": {
26
+ "node": ">=18.0.0"
27
+ }
28
+ }
package/preload.js ADDED
@@ -0,0 +1,26 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ const AUTH_FILE = path.join(__dirname, 'auth.json');
5
+
6
+ function loadAuth(alias) {
7
+ if (!fs.existsSync(AUTH_FILE)) {
8
+ throw new Error('Auth file not found, run auth first.');
9
+ }
10
+ const data = JSON.parse(fs.readFileSync(AUTH_FILE, 'utf-8'));
11
+ if (!data[alias]) {
12
+ throw new Error(`No auth found for alias: ${alias}`);
13
+ }
14
+ return { alias, ...data[alias] };
15
+ }
16
+
17
+ function saveAuth(alias, details) {
18
+ let data = {};
19
+ if (fs.existsSync(AUTH_FILE)) {
20
+ data = JSON.parse(fs.readFileSync(AUTH_FILE, 'utf-8'));
21
+ }
22
+ data[alias] = details;
23
+ fs.writeFileSync(AUTH_FILE, JSON.stringify(data, null, 2));
24
+ }
25
+
26
+ module.exports = { loadAuth, saveAuth };
package/renderer.js ADDED
@@ -0,0 +1,256 @@
1
+ const { execSync } = require('child_process');
2
+ const { saveAuth } = require('./preload');
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ /**
7
+ * Authenticate org (fetch auth details only, no login)
8
+ */
9
+ async function authenticate(alias) {
10
+ console.log(`🔐 Fetching auth details for org alias/username: ${alias}...`);
11
+ try {
12
+ const result = execSync(`sf force org display -o ${alias} --json`, {
13
+ encoding: 'utf8'
14
+ });
15
+ const json = JSON.parse(result);
16
+
17
+ const authDetails = {
18
+ alias,
19
+ accessToken: json.result.accessToken,
20
+ instanceUrl: json.result.instanceUrl,
21
+ apiVersion: json.result.apiVersion
22
+ };
23
+
24
+ saveAuth(alias, authDetails);
25
+ console.log(`💾 Saved auth for alias: ${alias}`);
26
+
27
+ // ✅ Return auth object for use in main.js
28
+ return authDetails;
29
+ } catch (err) {
30
+ console.error(`❌ Auth fetch failed for ${alias}: ${err.message}`);
31
+ throw err;
32
+ }
33
+ }
34
+
35
+
36
+ /**
37
+ * Fetch metadata list for a given type
38
+ */
39
+ async function fetchMetadata(auth, type) {
40
+ console.log(`📥 Fetching metadata type: ${type} from org: ${auth.alias}`);
41
+ try {
42
+ const result = execSync(
43
+ `sf force mdapi listmetadata -m ${type} -o ${auth.alias} --json`,
44
+ { encoding: 'utf8' }
45
+ );
46
+ const json = JSON.parse(result);
47
+ console.log(JSON.stringify(json, null, 2));
48
+ } catch (err) {
49
+ console.error(`❌ Fetch failed: ${err.message}`);
50
+ }
51
+ }
52
+
53
+ /**
54
+ * Compare metadata between two orgs and generate TailwindCSS dark HTML report
55
+ */
56
+
57
+
58
+
59
+ async function compareMetadata(org1Auth, org2Auth, type) {
60
+ console.log(`⚖️ Comparing metadata type: ${type} between ${org1Auth.alias} and ${org2Auth.alias}...`);
61
+ try {
62
+ // Fetch metadata from both orgs
63
+ const result1 = execSync(
64
+ `sf force mdapi listmetadata -m ${type} -o ${org1Auth.alias} --json`,
65
+ { encoding: 'utf8' }
66
+ );
67
+ const result2 = execSync(
68
+ `sf force mdapi listmetadata -m ${type} -o ${org2Auth.alias} --json`,
69
+ { encoding: 'utf8' }
70
+ );
71
+
72
+ const org1Meta = JSON.parse(result1);
73
+ const org2Meta = JSON.parse(result2);
74
+
75
+ const names1 = new Set((org1Meta.result || []).map(m => m.fullName));
76
+ const names2 = new Set((org2Meta.result || []).map(m => m.fullName));
77
+
78
+ const onlyInOrg1 = [...names1].filter(x => !names2.has(x));
79
+ const onlyInOrg2 = [...names2].filter(x => !names1.has(x));
80
+ const common = [...names1].filter(x => names2.has(x));
81
+
82
+ console.log(`✅ Comparison Done`);
83
+ console.log(`Common: ${common.length}, Only in ${org1Auth.alias}: ${onlyInOrg1.length}, Only in ${org2Auth.alias}: ${onlyInOrg2.length}`);
84
+
85
+ // -----------------------
86
+ // Export CSV to disk
87
+ // -----------------------
88
+ const csvLines = [];
89
+ csvLines.push('Type,Metadata Name');
90
+ onlyInOrg1.forEach(name => csvLines.push(`Only in ${org1Auth.alias},${name}`));
91
+ onlyInOrg2.forEach(name => csvLines.push(`Only in ${org2Auth.alias},${name}`));
92
+ common.forEach(name => csvLines.push(`Common,${name}`));
93
+
94
+ const csvPath = path.join(process.cwd(), `metadata-compare-${type}.csv`);
95
+ fs.writeFileSync(csvPath, csvLines.join('\n'), 'utf8');
96
+ console.log(`📄 CSV exported: ${csvPath}`);
97
+
98
+ // -----------------------
99
+ // Generate HTML Report
100
+ // -----------------------
101
+ const html = `
102
+ <!DOCTYPE html>
103
+ <html lang="en" class="dark">
104
+ <head>
105
+ <meta charset="UTF-8">
106
+ <title>Metadata Comparison - ${type}</title>
107
+ <script src="https://cdn.tailwindcss.com"></script>
108
+ <link rel="icon" type="image/x-icon"
109
+ href="https://mohan-chinnappan-n5.github.io/dfv/img/mc_favIcon.ico" />
110
+ <link rel="stylesheet" href="https://cdn.datatables.net/1.13.8/css/jquery.dataTables.min.css">
111
+ <link rel="stylesheet" href="https://cdn.datatables.net/responsive/2.5.0/css/responsive.dataTables.min.css">
112
+ <style>
113
+ body { background-color: #111827; color: #e5e7eb; }
114
+ table.dataTable tbody tr { background-color: #1f2937; }
115
+ table.dataTable tbody tr.odd { background-color: #111827; }
116
+ table.dataTable thead { background-color: #374151; color: #f3f4f6; }
117
+ </style>
118
+ </head>
119
+ <body class="p-8">
120
+
121
+ <h1 class="text-4xl font-bold mb-8 text-center">Metadata Comparison: ${type}</h1>
122
+
123
+ <!-- KPI Section -->
124
+ <div class="grid grid-cols-3 gap-6 mb-8 text-center">
125
+ <div class="bg-gray-800 rounded-lg p-6 border border-gray-700">
126
+ <div class="text-5xl font-extrabold text-blue-400">${common.length}</div>
127
+ <div class="text-gray-300 mt-2">Common</div>
128
+ </div>
129
+ <div class="bg-gray-800 rounded-lg p-6 border border-gray-700">
130
+ <div class="text-5xl font-extrabold text-red-400">${onlyInOrg1.length}</div>
131
+ <div class="text-gray-300 mt-2">Only in ${org1Auth.alias}</div>
132
+ </div>
133
+ <div class="bg-gray-800 rounded-lg p-6 border border-gray-700">
134
+ <div class="text-5xl font-extrabold text-green-400">${onlyInOrg2.length}</div>
135
+ <div class="text-gray-300 mt-2">Only in ${org2Auth.alias}</div>
136
+ </div>
137
+ </div>
138
+
139
+ <!-- Data Tables with CSV buttons -->
140
+ <div class="grid grid-cols-2 gap-6">
141
+ <div>
142
+ <div class="flex justify-between items-center mb-2">
143
+ <h2 class="text-xl font-semibold">Only in ${org1Auth.alias} (${onlyInOrg1.length})</h2>
144
+ <button id="download-org1" class="px-3 py-1 bg-blue-600 hover:bg-blue-700 text-white text-sm rounded-md">📥 CSV</button>
145
+ </div>
146
+ <table id="table-org1" class="display stripe hover w-full">
147
+ <thead><tr><th>Metadata Name</th></tr></thead>
148
+ <tbody>${onlyInOrg1.map(name => `<tr><td>${name}</td></tr>`).join('')}</tbody>
149
+ </table>
150
+ </div>
151
+
152
+ <div>
153
+ <div class="flex justify-between items-center mb-2">
154
+ <h2 class="text-xl font-semibold">Only in ${org2Auth.alias} (${onlyInOrg2.length})</h2>
155
+ <button id="download-org2" class="px-3 py-1 bg-blue-600 hover:bg-blue-700 text-white text-sm rounded-md">📥 CSV</button>
156
+ </div>
157
+ <table id="table-org2" class="display stripe hover w-full">
158
+ <thead><tr><th>Metadata Name</th></tr></thead>
159
+ <tbody>${onlyInOrg2.map(name => `<tr><td>${name}</td></tr>`).join('')}</tbody>
160
+ </table>
161
+ </div>
162
+ </div>
163
+
164
+ <div class="mt-10">
165
+ <div class="flex justify-between items-center mb-2">
166
+ <h2 class="text-xl font-semibold">Common Metadata (${common.length})</h2>
167
+ <button id="download-common" class="px-3 py-1 bg-blue-600 hover:bg-blue-700 text-white text-sm rounded-md">📥 CSV</button>
168
+ </div>
169
+ <table id="table-common" class="display stripe hover w-full">
170
+ <thead><tr><th>Metadata Name</th></tr></thead>
171
+ <tbody>${common.map(name => `<tr><td>${name}</td></tr>`).join('')}</tbody>
172
+ </table>
173
+ </div>
174
+
175
+ <script src="https://code.jquery.com/jquery-3.7.0.min.js"></script>
176
+ <script src="https://cdn.datatables.net/1.13.8/js/jquery.dataTables.min.js"></script>
177
+ <script src="https://cdn.datatables.net/responsive/2.5.0/js/dataTables.responsive.min.js"></script>
178
+ <script>
179
+ $(document).ready(function() {
180
+ const tableOrg1 = $('#table-org1').DataTable({ responsive: true });
181
+ const tableOrg2 = $('#table-org2').DataTable({ responsive: true });
182
+ const tableCommon = $('#table-common').DataTable({ responsive: true });
183
+
184
+ function downloadCSV(filename, rows) {
185
+ const csvContent = "data:text/csv;charset=utf-8,"
186
+ + rows.map(e => e.join(",")).join("\\n");
187
+ const encodedUri = encodeURI(csvContent);
188
+ const link = document.createElement("a");
189
+ link.setAttribute("href", encodedUri);
190
+ link.setAttribute("download", filename);
191
+ document.body.appendChild(link);
192
+ link.click();
193
+ document.body.removeChild(link);
194
+ }
195
+
196
+ // Per-table CSV buttons (fixed to get all rows)
197
+ $('#download-org1').on('click', () => {
198
+ const rows = tableOrg1.rows({ search: 'applied' }).data().toArray().map(r => [r[0]]);
199
+ downloadCSV('only-in-${org1Auth.alias}-${type}.csv', rows);
200
+ });
201
+
202
+ $('#download-org2').on('click', () => {
203
+ const rows = tableOrg2.rows({ search: 'applied' }).data().toArray().map(r => [r[0]]);
204
+ downloadCSV('only-in-${org2Auth.alias}-${type}.csv', rows);
205
+ });
206
+
207
+ $('#download-common').on('click', () => {
208
+ const rows = tableCommon.rows({ search: 'applied' }).data().toArray().map(r => [r[0]]);
209
+ downloadCSV('common-${type}.csv', rows);
210
+ });
211
+ });
212
+ </script>
213
+
214
+ </body>
215
+ </html>
216
+ `;
217
+
218
+ const htmlPath = path.join(process.cwd(), `metadata-compare-${type}.html`);
219
+ fs.writeFileSync(htmlPath, html, 'utf8');
220
+ console.log(`📄 HTML report generated: ${htmlPath}`);
221
+ execSync(`open ${htmlPath}`);
222
+
223
+ } catch (err) {
224
+ console.error(`❌ Compare failed: ${err.message}`);
225
+ }
226
+ }
227
+
228
+
229
+
230
+
231
+
232
+
233
+
234
+
235
+ /**
236
+ * Deploy metadata to target org
237
+ */
238
+ async function deployMetadata(auth, file) {
239
+ console.log(`🚀 Deploying ${file} to org: ${auth.alias}`);
240
+ try {
241
+ execSync(
242
+ `sf project deploy start -o ${auth.alias} -d ${file}`,
243
+ { stdio: 'inherit' }
244
+ );
245
+ console.log(`✅ Deploy completed`);
246
+ } catch (err) {
247
+ console.error(`❌ Deploy failed: ${err.message}`);
248
+ }
249
+ }
250
+
251
+ module.exports = {
252
+ authenticate,
253
+ fetchMetadata,
254
+ compareMetadata,
255
+ deployMetadata
256
+ };
package/sample_run.sh ADDED
@@ -0,0 +1,2 @@
1
+ node main.js auth -s mchinnappan@salesforce.com.devmerge -t mchinnappan@salesforce.com.p2o
2
+ node main.js compare -s mchinnappan@salesforce.com.devmerge -t mchinnappan@salesforce.com.p2o -m PermissionSet
package/tooldoc.html ADDED
@@ -0,0 +1,113 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en" class="dark">
3
+
4
+ <head>
5
+ <meta charset="UTF-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <title>Salesforce Metadata Comparer - Documentation</title>
8
+
9
+ <!-- Tailwind CSS -->
10
+ <script src="https://cdn.tailwindcss.com"></script>
11
+ <script>
12
+ tailwind.config = {
13
+ darkMode: 'class',
14
+ theme: {
15
+ extend: {
16
+ colors: {
17
+ 'sf-blue': '#0176d3',
18
+ 'sf-dark-blue': '#014486',
19
+ 'sf-green': '#10b981',
20
+ 'sf-red': '#ef4444'
21
+ }
22
+ }
23
+ }
24
+ }
25
+ </script>
26
+ </head>
27
+
28
+ <body class="bg-gray-900 text-gray-100 flex flex-col min-h-screen">
29
+
30
+ <!-- Header -->
31
+ <header class="sticky top-0 bg-gray-800 border-b border-gray-700 p-6 flex justify-between items-center z-50">
32
+ <h1 class="text-3xl font-bold text-sf-blue">Salesforce Metadata Comparer</h1>
33
+ </header>
34
+
35
+ <!-- Main Content -->
36
+ <main class="flex-1 p-6 space-y-12">
37
+
38
+ <!-- Introduction -->
39
+ <section>
40
+ <h2 class="text-2xl font-semibold mb-4 text-gray-200">Overview</h2>
41
+ <p class="text-gray-300">
42
+ The Salesforce Metadata Comparer is a tool that helps Salesforce admins and developers
43
+ compare metadata between two Salesforce orgs. Quickly identify differences, track missing
44
+ components, and deploy changes efficiently.
45
+ </p>
46
+ </section>
47
+
48
+ <!-- Features -->
49
+ <section>
50
+ <h2 class="text-2xl font-semibold mb-4 text-gray-200">Features</h2>
51
+ <ul class="list-disc list-inside space-y-2 text-gray-300">
52
+ <li>Authenticate with multiple Salesforce orgs using CLI aliases.</li>
53
+ <li>Fetch metadata types like Apex Classes, Triggers, Objects, Flows, Layouts, Profiles, and more.</li>
54
+ <li>Compare metadata between two orgs and identify:</li>
55
+ <ul class="list-disc list-inside ml-6 space-y-1">
56
+ <li>Common components</li>
57
+ <li>Only in Source Org</li>
58
+ <li>Only in Target Org</li>
59
+ </ul>
60
+ <li>Display comparison results in interactive tables with search, sorting, and CSV export.</li>
61
+ <li>KPI summary section with large numbers for quick insights.</li>
62
+ <li>Deploy metadata changes to the target org directly from the tool.</li>
63
+ <li>Dark theme UI for comfortable viewing.</li>
64
+ </ul>
65
+ </section>
66
+
67
+ <!-- How to Use -->
68
+ <section>
69
+ <h2 class="text-2xl font-semibold mb-4 text-gray-200">How to Use</h2>
70
+ <ol class="list-decimal list-inside space-y-3 text-gray-300">
71
+ <li>Ensure you have Salesforce CLI installed and authenticated with your orgs using aliases.</li>
72
+ <li>Run the tool and provide the source and target org aliases:</li>
73
+ <pre class="bg-gray-800 p-3 rounded text-green-400">node main.js -s sourceOrgAlias -t targetOrgAlias</pre>
74
+ <li>Select the metadata type you want to compare.</li>
75
+ <li>View the KPI summary and metadata tables in the UI.</li>
76
+ <li>Use the CSV export buttons to download comparison results.</li>
77
+ <li>If needed, deploy selected metadata changes to the target org.</li>
78
+ </ol>
79
+ </section>
80
+
81
+ <!-- Tips -->
82
+ <section>
83
+ <h2 class="text-2xl font-semibold mb-4 text-gray-200">Tips</h2>
84
+ <ul class="list-disc list-inside space-y-2 text-gray-300">
85
+ <li>Always authenticate your orgs before running a comparison.</li>
86
+ <li>For large metadata sets, CSV export helps analyze differences externally.</li>
87
+ <li>Keep your Salesforce CLI and plugins up to date to avoid command errors.</li>
88
+ <li>Use the swap org button in the UI if you need to reverse source/target comparison.</li>
89
+ </ul>
90
+ </section>
91
+
92
+ <!-- FAQ -->
93
+ <section>
94
+ <h2 class="text-2xl font-semibold mb-4 text-gray-200">FAQ</h2>
95
+ <div class="space-y-4">
96
+ <div>
97
+ <h3 class="font-medium text-gray-300">Q: Which metadata types are supported?</h3>
98
+ <p class="text-gray-400">A: Apex Classes, Triggers, Custom Objects, Flows, Page Layouts, Profiles, Permission Sets, Validation Rules, and more.</p>
99
+ <a href="https://mchinnappan100.github.io/pages2/metadata/search.html">View all supported metadata types</a>
100
+ </div>
101
+ </div>
102
+ </section>
103
+
104
+ </main>
105
+
106
+ <!-- Footer -->
107
+ <footer class="sticky bottom-0 bg-gray-800 border-t border-gray-700 p-4 text-center text-gray-400">
108
+ MC - Salesforce Metadata Comparer
109
+ </footer>
110
+
111
+ </body>
112
+
113
+ </html>