sfdx-hardis 6.0.7-beta202508172156.0 → 6.0.7-beta202508202323.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
@@ -4,10 +4,20 @@
4
4
 
5
5
  Note: Can be used with `sfdx plugins:install sfdx-hardis@beta` and docker image `hardisgroupcom/sfdx-hardis@beta`
6
6
 
7
+ - [hardis:org:refresh:before-refresh](https://sfdx-hardis.cloudity.com/hardis/org/refresh/before-refresh/)
8
+ - Retrieve Certificates and other metadatas that could need to be restored
9
+ - Retrieve Custom Settings values
10
+ - [hardis:org:refresh:after-refresh](https://sfdx-hardis.cloudity.com/hardis/org/refresh/after-refresh/)
11
+ - Restore Certificates and other metadatas that could need to be restored
12
+ - Restore Custom Settings values
13
+ - Smart restore of SAML SSO Config by prompting the user to select a valid certificate
14
+ - Send path to command log file to WebSocketServer
15
+ - Improve startup performances by checking for sfdx-hardis upgrades every 6h and not every 15 mn!
16
+
7
17
  ## [6.0.6 (beta)] 2025-08-17
8
18
 
9
- - New command [hardis:org:refresh:save:connectedapp](https://sfdx-hardis.cloudity.com/hardis/org/refresh/save/connectedapp/) : Save Connected Apps before refreshing a sandbox.
10
- - New command [hardis:org:refresh:restore:connectedapp](https://sfdx-hardis.cloudity.com/hardis/org/refresh/restore/connectedapp/) : Restore Connected Apps after refreshing a sandbox.
19
+ - New command [hardis:org:refresh:before-refresh](https://sfdx-hardis.cloudity.com/hardis/org/refresh/before-refresh/) : Save Connected Apps before refreshing a sandbox.
20
+ - New command [hardis:org:refresh:after-refresh](https://sfdx-hardis.cloudity.com/hardis/org/refresh/after-refresh/) : Restore Connected Apps after refreshing a sandbox.
11
21
  - Update JSON Schema documentation
12
22
  - When authenticating to an expired org token, delete the SF Cli file that can mess with us when we refreshed a sandbox.
13
23
  - Improve logs display
@@ -0,0 +1,8 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <Package xmlns="http://soap.sforce.com/2006/04/metadata">
3
+ <types>
4
+ <members>*</members>
5
+ <name>Certificate</name>
6
+ </types>
7
+ <version>64.0</version>
8
+ </Package>
@@ -0,0 +1,44 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <Package xmlns="http://soap.sforce.com/2006/04/metadata">
3
+ <types>
4
+ <members>*</members>
5
+ <name>AuthProvider</name>
6
+ </types>
7
+ <types>
8
+ <members>*</members>
9
+ <name>CorsWhitelistOrigin</name>
10
+ </types>
11
+ <types>
12
+ <members>*</members>
13
+ <name>CustomMetadata</name>
14
+ </types>
15
+ <types>
16
+ <members>*</members>
17
+ <name>EmailServicesFunction</name>
18
+ </types>
19
+ <types>
20
+ <members>*</members>
21
+ <name>ExternalCredential</name>
22
+ </types>
23
+ <types>
24
+ <members>*</members>
25
+ <name>ExternalDataSource</name>
26
+ </types>
27
+ <types>
28
+ <members>*</members>
29
+ <name>NamedCredential</name>
30
+ </types>
31
+ <types>
32
+ <members>*</members>
33
+ <name>RemoteSiteSetting</name>
34
+ </types>
35
+ <types>
36
+ <members>*</members>
37
+ <name>SamlSsoConfig</name>
38
+ </types>
39
+ <types>
40
+ <members>*</members>
41
+ <name>WorkflowOutboundMessage</name>
42
+ </types>
43
+ <version>64.0</version>
44
+ </Package>
@@ -1,4 +1,5 @@
1
1
  import { SfCommand } from '@salesforce/sf-plugins-core';
2
+ import { Connection } from '@salesforce/core';
2
3
  import { AnyJson } from '@salesforce/ts-types';
3
4
  export default class OrgRefreshAfterRefresh extends SfCommand<AnyJson> {
4
5
  static title: string;
@@ -14,7 +15,19 @@ export default class OrgRefreshAfterRefresh extends SfCommand<AnyJson> {
14
15
  static requiresProject: boolean;
15
16
  protected refreshSandboxConfig: any;
16
17
  protected saveProjectPath: string;
18
+ protected result: any;
19
+ protected orgUsername: string;
20
+ protected nameFilter: string | undefined;
21
+ protected processAll: boolean;
22
+ protected conn: Connection;
23
+ protected instanceUrl: any;
24
+ protected orgId: string;
17
25
  run(): Promise<AnyJson>;
26
+ private restoreCertificates;
27
+ private restoreOtherMetadata;
28
+ private restoreSamlSsoConfig;
29
+ private restoreCustomSettings;
30
+ private restoreConnectedApps;
18
31
  private findConnectedAppsInProject;
19
32
  private selectConnectedApps;
20
33
  private deleteExistingConnectedApps;
@@ -2,14 +2,16 @@ import { SfCommand, Flags } from '@salesforce/sf-plugins-core';
2
2
  import { Messages } from '@salesforce/core';
3
3
  import * as path from 'path';
4
4
  import c from 'chalk';
5
- import * as fs from 'fs';
5
+ import fs from 'fs-extra';
6
6
  import { glob } from 'glob';
7
- import { uxLog } from '../../../../common/utils/index.js';
8
- import { parseXmlFile } from '../../../../common/utils/xmlUtils.js';
7
+ import { execSfdxJson, uxLog } from '../../../../common/utils/index.js';
8
+ import { parsePackageXmlFile, parseXmlFile, writePackageXmlFile } from '../../../../common/utils/xmlUtils.js';
9
9
  import { GLOB_IGNORE_PATTERNS } from '../../../../common/utils/projectUtils.js';
10
10
  import { deleteConnectedApps, deployConnectedApps, toConnectedAppFormat, validateConnectedApps, selectConnectedAppsForProcessing, createConnectedAppSuccessResponse, handleConnectedAppError } from '../../../../common/utils/refresh/connectedAppUtils.js';
11
11
  import { getConfig } from '../../../../config/index.js';
12
12
  import { prompts } from '../../../../common/utils/prompts.js';
13
+ import { WebSocketClient } from '../../../../common/websocketClient.js';
14
+ import { soqlQuery, soqlQueryTooling } from '../../../../common/utils/apiUtils.js';
13
15
  Messages.importMessagesDirectoryFromMetaUrl(import.meta.url);
14
16
  const messages = Messages.loadMessages('sfdx-hardis', 'org');
15
17
  export default class OrgRefreshAfterRefresh extends SfCommand {
@@ -79,18 +81,32 @@ This command is part of [sfdx-hardis Sandbox Refresh](https://sfdx-hardis.cloudi
79
81
  static requiresProject = true;
80
82
  refreshSandboxConfig = {};
81
83
  saveProjectPath;
84
+ result;
85
+ orgUsername;
86
+ nameFilter;
87
+ processAll;
88
+ conn;
89
+ instanceUrl;
90
+ orgId;
82
91
  async run() {
83
92
  const { flags } = await this.parse(OrgRefreshAfterRefresh);
84
- const orgUsername = flags["target-org"].getUsername();
85
- const conn = flags["target-org"].getConnection();
86
- const instanceUrl = conn.instanceUrl;
93
+ this.orgUsername = flags["target-org"].getUsername();
94
+ this.conn = flags["target-org"].getConnection();
95
+ this.orgId = flags["target-org"].getOrgId();
96
+ this.instanceUrl = this.conn.instanceUrl;
87
97
  /* jscpd:ignore-start */
88
- const processAll = flags.all || false;
89
- const nameFilter = processAll ? undefined : flags.name; // If --all is set, ignore --name
98
+ this.processAll = flags.all || false;
99
+ this.nameFilter = this.processAll ? undefined : flags.name; // If --all is set, ignore --name
90
100
  const config = await getConfig("user");
91
101
  this.refreshSandboxConfig = config?.refreshSandboxConfig || {};
102
+ this.result = {};
92
103
  /* jscpd:ignore-end */
93
- uxLog("action", this, c.cyan(`This command with restore information after the refresh of org ${instanceUrl}`));
104
+ uxLog("action", this, c.cyan(`This command will restore information after the refresh of org ${this.instanceUrl}
105
+ - Certificates
106
+ - Other Metadatas
107
+ - SAML SSO Config
108
+ - Custom Settings
109
+ - Connected Apps`));
94
110
  // Prompt user to select a save project path
95
111
  const saveProjectPathRoot = path.join(process.cwd(), 'scripts', 'sandbox-refresh');
96
112
  // Only get immediate subfolders of saveProjectPathRoot (not recursive)
@@ -108,33 +124,420 @@ This command is part of [sfdx-hardis Sandbox Refresh](https://sfdx-hardis.cloudi
108
124
  })),
109
125
  });
110
126
  this.saveProjectPath = saveProjectPath.path;
127
+ // 1. Restore Certificates
128
+ await this.restoreCertificates();
129
+ // 2. Restore Other Metadata
130
+ await this.restoreOtherMetadata();
131
+ // 3. Restore SamlSsoConfig
132
+ await this.restoreSamlSsoConfig();
133
+ // 4. Restore Custom Settings
134
+ await this.restoreCustomSettings();
135
+ // 5. Restore Connected Apps
136
+ await this.restoreConnectedApps();
137
+ return this.result;
138
+ }
139
+ async restoreCertificates() {
140
+ const certsDir = path.join(this.saveProjectPath, 'force-app', 'main', 'default', 'certs');
141
+ const manifestDir = path.join(this.saveProjectPath, 'manifest');
142
+ const certsPackageXml = path.join(manifestDir, 'package-certificates-to-save.xml');
143
+ if (!fs.existsSync(certsDir) || !fs.existsSync(certsPackageXml)) {
144
+ uxLog("log", this, c.yellow('No certificates backup found, skipping certificate restore.'));
145
+ return;
146
+ }
147
+ // Copy certs to a temporary folder for deployment
148
+ const mdApiCertsRestoreFolder = path.join(this.saveProjectPath, 'mdapi_certs_restore');
149
+ await fs.ensureDir(mdApiCertsRestoreFolder);
150
+ await fs.emptyDir(mdApiCertsRestoreFolder);
151
+ await fs.copy(certsDir, path.join(mdApiCertsRestoreFolder, "certs"), { overwrite: true });
152
+ // List certificates in the restore folder
153
+ const certsFiles = fs.readdirSync(certsDir);
154
+ if (certsFiles.length === 0) {
155
+ uxLog("log", this, c.yellow('No certificates found in the backup folder, skipping certificate restore.'));
156
+ return;
157
+ }
158
+ // List .crt files and get their name, then check that each cert must have a .crt and a .crt-meta.xml file
159
+ const certsToRestoreNames = certsFiles.filter(file => file.endsWith('.crt')).map(file => path.basename(file, '.crt'));
160
+ const validCertsToRestoreNames = certsToRestoreNames.filter(name => {
161
+ return fs.existsSync(path.join(certsDir, `${name}.crt-meta.xml`));
162
+ });
163
+ if (validCertsToRestoreNames.length === 0) {
164
+ uxLog("log", this, c.yellow('No valid certificates found in the backup folder (with .crt + .crt-meta.xml), skipping certificate restore.'));
165
+ return;
166
+ }
167
+ // Prompt certificates to restore (all by default)
168
+ const promptCerts = await prompts({
169
+ type: 'multiselect',
170
+ name: 'certs',
171
+ message: `Select certificates to restore`,
172
+ description: 'Select the certificates you want to restore from the backup. You can select multiple certificates.',
173
+ choices: validCertsToRestoreNames.map(name => ({
174
+ title: name,
175
+ value: name
176
+ })),
177
+ initial: validCertsToRestoreNames, // Select all by default
178
+ });
179
+ const selectedCerts = promptCerts.certs;
180
+ if (selectedCerts.length === 0) {
181
+ uxLog("log", this, c.yellow('No certificates selected for restore, skipping certificate restore.'));
182
+ return;
183
+ }
184
+ // Ask user confirmation before restoring certificates
185
+ const prompt = await prompts({
186
+ type: 'confirm',
187
+ name: 'restore',
188
+ message: `Do you confirm you want to restore ${selectedCerts.length} certificate(s) ?`,
189
+ description: 'This will deploy all certificate files and definitions saved before the refresh.',
190
+ initial: true
191
+ });
192
+ if (!prompt.restore) {
193
+ return;
194
+ }
195
+ // Create manifest/package.xml within mdApiCertsRestoreFolder
196
+ const packageXmlCerts = {
197
+ "Certificate": selectedCerts
198
+ };
199
+ await writePackageXmlFile(path.join(mdApiCertsRestoreFolder, 'package.xml'), packageXmlCerts);
200
+ // Deploy using metadata API
201
+ uxLog("log", this, c.grey(`Deploying certificates in org ${this.instanceUrl} using Metadata API (Source Api does not support it)...`));
202
+ await execSfdxJson(`sf project deploy start --metadata-dir ${mdApiCertsRestoreFolder} --target-org ${this.orgUsername}`, this, { output: true, fail: true, cwd: this.saveProjectPath });
203
+ uxLog("success", this, c.green(`Certificates restored successfully in org ${this.instanceUrl}`));
204
+ }
205
+ async restoreOtherMetadata() {
206
+ const manifestDir = path.join(this.saveProjectPath, 'manifest');
207
+ const restorePackageXml = path.join(manifestDir, 'package-metadata-to-restore.xml');
208
+ // Check if the restore package.xml exists
209
+ if (!fs.existsSync(restorePackageXml)) {
210
+ uxLog("log", this, c.yellow('No package-metadata-to-restore.xml found, skipping metadata restore.'));
211
+ return;
212
+ }
213
+ // Warn user about the restore package.xml that needs to be manually checked
214
+ WebSocketClient.sendReportFileMessage(restorePackageXml, "Restore Metadatas package.xml", "report");
215
+ uxLog("action", this, c.cyan(`Now handling the restore of other metadata from ${restorePackageXml}...`));
216
+ const metadataRestore = await parsePackageXmlFile(restorePackageXml);
217
+ const metadataSummary = Object.keys(metadataRestore).map(key => {
218
+ return `${key}(${Array.isArray(metadataRestore[key]) ? metadataRestore[key].length : 0})`;
219
+ }).join(', ');
220
+ uxLog("warning", this, c.yellow(`Look at the package-metadata-to-restore.xml file in ${c.bold(this.saveProjectPath)} to see what will be restored.`));
221
+ uxLog("warning", this, c.yellow(`Confirm it's content, or remove/comment part of it if you don't want some metadata to be restored\n${metadataSummary}`));
222
+ const prompt = await prompts({
223
+ type: 'confirm',
224
+ name: 'restore',
225
+ message: `Please double check package-metadata-to-restore.xml. Do you confirm you want to restore all these metadatas ?\n${metadataSummary}`,
226
+ description: `WARNING: Check and validate/update file ${restorePackageXml} BEFORE it is deployed !`,
227
+ initial: true
228
+ });
229
+ if (!prompt.restore) {
230
+ uxLog("warning", this, c.yellow('Metadata restore cancelled by user.'));
231
+ this.result = Object.assign(this.result, { success: false, message: 'Metadata restore cancelled by user' });
232
+ return;
233
+ }
234
+ // Deploy the metadata using the package.xml
235
+ uxLog("action", this, c.cyan('Deploying other metadatas to org...'));
236
+ const deployCmd = `sf project deploy start --manifest ${restorePackageXml} --target-org ${this.orgUsername} --json`;
237
+ const deployResult = await execSfdxJson(deployCmd, this, { output: true, fail: true, cwd: this.saveProjectPath });
238
+ if (deployResult.status === 0) {
239
+ uxLog("success", this, c.green(`Other metadata restored successfully in org ${this.instanceUrl}`));
240
+ }
241
+ else {
242
+ uxLog("error", this, c.red(`Failed to restore other metadata in org ${this.instanceUrl}: ${deployResult.error}`));
243
+ this.result = Object.assign(this.result, { success: false, message: `Failed to restore other metadata: ${deployResult.error}` });
244
+ throw new Error(`Failed to restore other metadata:\n${JSON.stringify(deployResult, null, 2)}`);
245
+ }
246
+ }
247
+ async restoreSamlSsoConfig() {
248
+ // 0. List all samlssoconfigs in the project, prompt user to select which to restore
249
+ const samlDir = path.join(this.saveProjectPath, 'force-app', 'main', 'default', 'samlssoconfigs');
250
+ if (!fs.existsSync(samlDir)) {
251
+ uxLog("action", this, c.cyan('No SAML SSO Configs found, skipping SAML SSO config restore.'));
252
+ return;
253
+ }
254
+ const allSamlFiles = fs.readdirSync(samlDir).filter(f => f.endsWith('.samlssoconfig-meta.xml'));
255
+ if (allSamlFiles.length === 0) {
256
+ uxLog("action", this, c.yellow('No SAML SSO Config XML files found., skipping SAML SSO config restore.'));
257
+ return;
258
+ }
259
+ // Prompt user to select which SAML SSO configs to restore
260
+ const promptSaml = await prompts({
261
+ type: 'multiselect',
262
+ name: 'samlFiles',
263
+ message: 'Select SAML SSO Configs to restore',
264
+ description: 'Select the SAML SSO Configs you want to restore from the backup. You can select multiple configs.',
265
+ choices: allSamlFiles.map(f => ({ title: f.replace('.samlssoconfig-meta.xml', ''), value: f })),
266
+ initial: allSamlFiles // select all by default
267
+ });
268
+ const selectedSamlFiles = promptSaml.samlFiles;
269
+ if (!selectedSamlFiles || selectedSamlFiles.length === 0) {
270
+ uxLog("log", this, c.yellow('No SAML SSO Configs selected for restore, skipping.'));
271
+ return;
272
+ }
273
+ // 1. Clean up XML and prompt for cert
274
+ // Query active certificates
275
+ const soql = "SELECT Id, MasterLabel FROM Certificate WHERE ExpirationDate > TODAY LIMIT 200";
276
+ let certs = [];
111
277
  try {
112
- // Step 1: Find Connected Apps in the project
113
- const connectedApps = await this.findConnectedAppsInProject(nameFilter, processAll);
114
- if (connectedApps.length === 0) {
115
- uxLog("warning", this, c.yellow('No Connected Apps found in the project'));
116
- return { success: false, message: 'No Connected Apps found in the project' };
278
+ const res = await soqlQueryTooling(soql, this.conn);
279
+ certs = res.records;
280
+ }
281
+ catch (e) {
282
+ uxLog("error", this, c.red(`Failed to query active certificates: ${e}`));
283
+ return;
284
+ }
285
+ if (!certs.length) {
286
+ uxLog("error", this, c.yellow('No active certificates found in org. You\'ll need to update manually field requestSigningCertId with the id of a valid certificate.'));
287
+ return;
288
+ }
289
+ const updated = [];
290
+ const errors = [];
291
+ for (const samlFile of selectedSamlFiles) {
292
+ const samlName = samlFile.replace('.samlssoconfig-meta.xml', '');
293
+ // Prompt user to select a certificate
294
+ const certPrompt = await prompts({
295
+ type: 'select',
296
+ name: 'certId',
297
+ message: `Select the certificate to use for SAML SSO config ${samlName}`,
298
+ description: `This will update <requestSigningCertId> in ${samlFile}.`,
299
+ choices: certs.map(cert => ({
300
+ title: cert.MasterLabel,
301
+ value: cert.Id.substring(0, 15)
302
+ })),
303
+ });
304
+ const selectedCertId = certPrompt.certId;
305
+ if (!selectedCertId) {
306
+ uxLog("warning", this, c.yellow('No certificate selected. Skipping SAML SSO config update.'));
307
+ errors.push(`No certificate selected for ${samlName}`);
308
+ continue;
117
309
  }
118
- /* jscpd:ignore-start */
119
- // Step 2: Select which Connected Apps to process
120
- const selectedApps = await this.selectConnectedApps(connectedApps, processAll, nameFilter);
121
- if (selectedApps.length === 0) {
122
- uxLog("warning", this, c.yellow('No Connected Apps selected'));
123
- return { success: false, message: 'No Connected Apps selected' };
310
+ const filePath = path.join(samlDir, samlFile);
311
+ let xml = await fs.readFile(filePath, 'utf8');
312
+ // Remove <oauthTokenEndpoint>...</oauthTokenEndpoint>
313
+ xml = xml.replace(/<oauthTokenEndpoint>.*?<\/oauthTokenEndpoint>\s*/gs, '');
314
+ // Remove <salesforceLoginUrl>...</salesforceLoginUrl>
315
+ xml = xml.replace(/<salesforceLoginUrl>.*?<\/salesforceLoginUrl>\s*/gs, '');
316
+ // Replace <requestSigningCertId>...</requestSigningCertId>
317
+ if (/<requestSigningCertId>.*?<\/requestSigningCertId>/s.test(xml)) {
318
+ xml = xml.replace(/<requestSigningCertId>.*?<\/requestSigningCertId>/s, `<requestSigningCertId>${selectedCertId}</requestSigningCertId>`);
319
+ }
320
+ await fs.writeFile(filePath, xml, 'utf8');
321
+ uxLog("log", this, c.grey(`Updated SAML SSO config ${samlFile} with certificate ${selectedCertId} and removed readonly tags oauthTokenEndpoint & salesforceLoginUrl`));
322
+ // 2. Prompt user to confirm deployment
323
+ const promptDeploy = await prompts({
324
+ type: 'confirm',
325
+ name: 'deploy',
326
+ message: `Do you confirm you want to deploy ${samlFile} SAML SSO Config to the org?`,
327
+ description: 'This will deploy the selected SAML SSO Configs to the org using SFDX',
328
+ initial: true
329
+ });
330
+ if (!promptDeploy.deploy) {
331
+ uxLog("warning", this, c.yellow(`SAML SSO Config ${samlFile} deployment cancelled by user.`));
332
+ errors.push(`Deployment cancelled for ${samlFile}`);
333
+ continue;
334
+ }
335
+ const deployCommand = `sf project deploy start -m SamlSsoConfig:${samlName} --target-org ${this.orgUsername}`;
336
+ try {
337
+ uxLog("action", this, c.cyan(`Deploying SAML SSO Config ${samlName} to org ${this.instanceUrl}...`));
338
+ const deployResult = await execSfdxJson(deployCommand, this, { output: true, fail: true, cwd: this.saveProjectPath });
339
+ if (deployResult.status === 0) {
340
+ uxLog("success", this, c.green(`SAML SSO Config ${samlName} deployed successfully in org ${this.instanceUrl}`));
341
+ updated.push(samlName);
342
+ }
343
+ else {
344
+ uxLog("error", this, c.red(`Failed to deploy SAML SSO Config ${samlName}: ${deployResult.error}`));
345
+ errors.push(`Failed to deploy ${samlName}: ${deployResult.error}`);
346
+ }
347
+ }
348
+ catch (e) {
349
+ uxLog("error", this, c.red(`Error deploying SAML SSO Config ${samlName}: ${e.message}`));
350
+ errors.push(`Error deploying ${samlName}: ${e.message}`);
124
351
  }
125
- /* jscpd:ignore-end */
126
- // Step 3: Delete existing Connected Apps from the org for clean deployment
127
- await this.deleteExistingConnectedApps(orgUsername, selectedApps);
128
- // Step 4: Deploy the Connected Apps to the org
129
- await this.deployConnectedApps(orgUsername, selectedApps);
130
- // Return the result
131
- uxLog("action", this, c.cyan(`Summary`));
132
- const appNames = selectedApps.map(app => `- ${app.fullName}`).join('\n');
133
- uxLog("success", this, c.green(`Successfully restored ${selectedApps.length} Connected App(s) to ${conn.instanceUrl}\n${appNames}`));
134
- return createConnectedAppSuccessResponse(`Successfully restored ${selectedApps.length} Connected App(s) to the org`, selectedApps.map(app => app.fullName));
135
352
  }
136
- catch (error) {
137
- return handleConnectedAppError(error, this);
353
+ // 3. Summary of results
354
+ uxLog("action", this, c.cyan(`SAML SSO Config processing completed.`));
355
+ if (updated.length > 0) {
356
+ uxLog("success", this, c.green(`Successfully updated and deployed SAML SSO Configs: ${updated.join(', ')}`));
357
+ }
358
+ if (errors.length > 0) {
359
+ uxLog("error", this, c.red(`Errors occurred during SAML SSO Config processing:\n${errors.join('\n')}`));
360
+ this.result = Object.assign(this.result, { success: false, message: `SAML SSO Config processing errors:\n${errors.join('\n')}` });
361
+ }
362
+ }
363
+ async restoreCustomSettings() {
364
+ // Check there are custom settings to restore
365
+ const csDir = path.join(this.saveProjectPath, 'savedCustomSettings');
366
+ if (!fs.existsSync(csDir)) {
367
+ uxLog("log", this, c.yellow('No savedCustomSettings folder found, skipping custom settings restore.'));
368
+ return;
369
+ }
370
+ const csFolders = fs.readdirSync(csDir).filter(f => fs.statSync(path.join(csDir, f)).isDirectory());
371
+ if (csFolders.length === 0) {
372
+ uxLog("log", this, c.yellow('No custom settings data found, skipping custom settings restore.'));
373
+ return;
374
+ }
375
+ // List custom settings to restore so users can select them. Keep only folders that have a .json file
376
+ const csToRestore = csFolders.filter(folder => {
377
+ const jsonFile = path.join(csDir, folder, `${folder}.json`);
378
+ return fs.existsSync(jsonFile);
379
+ });
380
+ if (csToRestore.length === 0) {
381
+ uxLog("log", this, c.yellow('No custom settings data found to restore, skipping custom settings restore.'));
382
+ return;
383
+ }
384
+ // Prompt custom settings to restore: All by default
385
+ const promptRestore = await prompts({
386
+ type: 'multiselect',
387
+ name: 'settings',
388
+ message: `Select custom settings to restore`,
389
+ description: 'Select the custom settings you want to restore from the backup. You can select multiple settings.',
390
+ choices: csToRestore.map(folder => ({
391
+ title: folder,
392
+ value: folder
393
+ })),
394
+ initial: csToRestore // Select all by default
395
+ });
396
+ const selectedSettings = promptRestore.settings;
397
+ if (selectedSettings.length === 0) {
398
+ uxLog("log", this, c.yellow('No custom settings selected for restore, skipping custom settings restore.'));
399
+ return;
400
+ }
401
+ // Ask last confirmation to user
402
+ const prompt = await prompts({
403
+ type: 'confirm',
404
+ name: 'restore',
405
+ message: `Do you confirm you want to restore ${selectedSettings.length} Custom Settings values from backup?`,
406
+ description: 'This will import all custom settings data saved before the refresh.',
407
+ initial: true
408
+ });
409
+ if (!prompt.restore) {
410
+ uxLog("warning", this, c.yellow('Custom settings restore cancelled by user.'));
411
+ return;
412
+ }
413
+ uxLog("action", this, c.cyan(`Restoring ${selectedSettings.length} Custom Settings...`));
414
+ const successSettings = [];
415
+ const failedSettings = [];
416
+ for (const folder of selectedSettings) {
417
+ const jsonFile = path.join(csDir, folder, `${folder}.json`);
418
+ if (!fs.existsSync(jsonFile)) {
419
+ uxLog("warning", this, c.yellow(`No data file for custom setting ${folder}`));
420
+ failedSettings.push(folder);
421
+ continue;
422
+ }
423
+ // Remove standard fields from the JSON file and create a new file without them, and replace Org Id with the current org one
424
+ const jsonFileForImport = path.join(csDir, folder, `${folder}-without-standard-fields.json`);
425
+ const jsonData = await fs.readJson(jsonFile);
426
+ const standardFields = ['LastModifiedDate', 'IsDeleted', 'CreatedById', 'CreatedDate', 'LastModifiedById', 'SystemModstamp'];
427
+ let deleteExistingCsBefore = false;
428
+ jsonData.records = (jsonData?.records || []).map((record) => {
429
+ const newRecord = {};
430
+ for (const key in record) {
431
+ // Remove standard fields
432
+ if (!standardFields.includes(key)) {
433
+ newRecord[key] = record[key];
434
+ }
435
+ // Replace Org Id with the current org one
436
+ if (key === 'SetupOwnerId') {
437
+ newRecord[key] = this.orgId; // Replace with current org Id
438
+ deleteExistingCsBefore = true; // Use upsert if SetupOwnerId is present
439
+ }
440
+ }
441
+ return newRecord;
442
+ });
443
+ // Write the new JSON file without standard fields
444
+ await fs.writeJson(jsonFileForImport, jsonData, { spaces: 2 });
445
+ // Delete existing custom settings before import if needed
446
+ if (deleteExistingCsBefore) {
447
+ uxLog("log", this, c.grey(`Deleting existing custom settings for ${folder} in org ${this.orgUsername} before import...`));
448
+ // Query existing custom settings to delete
449
+ const query = `SELECT Id FROM ${folder} WHERE SetupOwnerId = '${this.orgId}'`;
450
+ const queryRes = await soqlQuery(query, this.conn);
451
+ if (queryRes.records.length > 0) {
452
+ const idsToDelete = (queryRes?.records.map(record => record.Id) || []).filter((id) => typeof id === 'string');
453
+ uxLog("log", this, c.grey(`Found ${idsToDelete.length} existing custom settings to delete for ${folder} in org ${this.orgUsername}`));
454
+ const deleteResults = await this.conn.sobject(folder).destroy(idsToDelete, { allOrNone: true });
455
+ const deletedSuccessFullyIds = deleteResults.filter(result => result.success).map(result => "- " + result.id).join('\n');
456
+ uxLog("log", this, c.grey(`Deleted ${deletedSuccessFullyIds.length} existing custom settings for ${folder} in org ${this.orgUsername}\n${deletedSuccessFullyIds}`));
457
+ const deletedErrorIds = deleteResults.filter(result => !result.success).map(result => "- " + result.id).join('\n');
458
+ if (deletedErrorIds.length > 0) {
459
+ uxLog("warning", this, c.yellow(`Failed to delete existing custom settings for ${folder} in org ${this.orgUsername}\n${deletedErrorIds}`));
460
+ continue; // Skip to next setting if deletion failed
461
+ }
462
+ }
463
+ else {
464
+ uxLog("log", this, c.grey(`No existing custom settings found for ${folder} in org ${this.orgUsername}.`));
465
+ }
466
+ }
467
+ // Import the custom setting using sf data tree import
468
+ const importCmd = `sf data tree import --files ${jsonFileForImport} --target-org ${this.orgUsername} --json`;
469
+ try {
470
+ const importRes = await execSfdxJson(importCmd, this, { output: true, fail: true, cwd: this.saveProjectPath });
471
+ if (importRes.status === 0) {
472
+ uxLog("success", this, c.green(`Custom setting ${folder} restored.`));
473
+ successSettings.push(folder);
474
+ }
475
+ else {
476
+ uxLog("error", this, c.red(`Failed to restore custom setting ${folder}:\n${JSON.stringify(importRes, null, 2)}`));
477
+ failedSettings.push(folder);
478
+ }
479
+ }
480
+ catch (e) {
481
+ uxLog("error", this, c.red(`Custom setting ${folder} restore failed:\n${JSON.stringify(e)}`));
482
+ failedSettings.push(folder);
483
+ continue;
484
+ }
485
+ }
486
+ uxLog("action", this, c.cyan(`Custom settings restore complete (${successSettings.length} successful, ${failedSettings.length} failed)`));
487
+ if (successSettings.length > 0) {
488
+ const successSettingsNames = successSettings.map(name => "- " + name).join('\n');
489
+ uxLog("success", this, c.green(`Successfully restored ${successSettings.length} Custom Setting(s):\n ${successSettingsNames}`));
490
+ }
491
+ if (failedSettings.length > 0) {
492
+ const failedSettingsNames = failedSettings.map(name => "- " + name).join('\n');
493
+ uxLog("error", this, c.red(`Failed to restore ${failedSettings.length} Custom Setting(s): ${failedSettingsNames}`));
494
+ }
495
+ }
496
+ async restoreConnectedApps() {
497
+ let restoreConnectedApps = false;
498
+ const promptRestoreConnectedApps = await prompts({
499
+ type: 'confirm',
500
+ name: 'confirmRestore',
501
+ message: `Do you want to restore Connected Apps from the backup in ${c.bold(this.saveProjectPath)}?`,
502
+ initial: true,
503
+ description: 'This will restore all Connected Apps (including Consumer Secrets) from the backup created before the org refresh.'
504
+ });
505
+ if (promptRestoreConnectedApps.confirmRestore) {
506
+ restoreConnectedApps = true;
507
+ }
508
+ if (restoreConnectedApps) {
509
+ try {
510
+ // Step 1: Find Connected Apps in the project
511
+ const connectedApps = await this.findConnectedAppsInProject(this.nameFilter, this.processAll);
512
+ if (connectedApps.length === 0) {
513
+ uxLog("warning", this, c.yellow('No Connected Apps found in the project'));
514
+ this.result = Object.assign(this.result, { success: false, message: 'No Connected Apps found in the project' });
515
+ return;
516
+ }
517
+ /* jscpd:ignore-start */
518
+ // Step 2: Select which Connected Apps to process
519
+ const selectedApps = await this.selectConnectedApps(connectedApps, this.processAll, this.nameFilter);
520
+ if (selectedApps.length === 0) {
521
+ uxLog("warning", this, c.yellow('No Connected Apps selected'));
522
+ this.result = Object.assign(this.result, { success: false, message: 'No Connected Apps selected' });
523
+ return;
524
+ }
525
+ /* jscpd:ignore-end */
526
+ // Step 3: Delete existing Connected Apps from the org for clean deployment
527
+ await this.deleteExistingConnectedApps(this.orgUsername, selectedApps);
528
+ // Step 4: Deploy the Connected Apps to the org
529
+ await this.deployConnectedApps(this.orgUsername, selectedApps);
530
+ // Return the result
531
+ uxLog("action", this, c.cyan(`Summary`));
532
+ const appNames = selectedApps.map(app => `- ${app.fullName}`).join('\n');
533
+ uxLog("success", this, c.green(`Successfully restored ${selectedApps.length} Connected App(s) to ${this.conn.instanceUrl}\n${appNames}`));
534
+ const restoreResult = createConnectedAppSuccessResponse(`Successfully restored ${selectedApps.length} Connected App(s) to the org`, selectedApps.map(app => app.fullName));
535
+ this.result = Object.assign(this.result, restoreResult);
536
+ }
537
+ catch (error) {
538
+ const restoreResult = handleConnectedAppError(error, this);
539
+ this.result = Object.assign(this.result, restoreResult);
540
+ }
138
541
  }
139
542
  }
140
543
  async findConnectedAppsInProject(nameFilter, processAll) {