sfdx-hardis 6.4.4 → 6.5.1

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,6 +4,23 @@
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
+ ## [6.5.1] 2025-09-20
8
+
9
+ - [hardis:org:monitor:backup](https://sfdx-hardis.cloudity.com/hardis/org/monitor/backup/) enhancements:
10
+ - Creates the 'force-app/main/default' directory if it doesn't exist before retrieving metadatas
11
+ - [hardis:org:configure:monitoring](https://sfdx-hardis.cloudity.com/hardis/org/configure/monitoring/):
12
+ - Display the connected App XML in logs (while hiding sensitive info)
13
+ - When production org, run the first found test class (or allow to force its selection using ENV variable `SFDX_HARDIS_TECH_DEPLOY_TEST_CLASS` )
14
+ - Add instructions to use ghcr.io Docker image in case of rate limits reached on Docker Hub
15
+ - Handle progress component in UI when generating documentation
16
+
17
+ ## [6.5.0] 2025-09-17
18
+
19
+ - Files export enhancements:
20
+ - Resume + validate downloaded files
21
+ - Improves API limit handling for file export/import
22
+ - When prompting for org url, allow to input just the domain (ex: `hardis-group`) and sfdx-hardis will build the rest of the url
23
+
7
24
  ## [6.4.4] 2025-09-16
8
25
 
9
26
  - When prompting for org instance URL, allow to copy-paste the full URL to gain time
@@ -29,7 +46,6 @@ Note: Can be used with `sfdx plugins:install sfdx-hardis@beta` and docker image
29
46
 
30
47
  - Allow to override Bulk API v2 settings with env variables **BULKAPIV2_POLL_INTERVAL**, **BULKAPIV2_POLL_TIMEOUT** and **BULK_QUERY_RETRY**
31
48
 
32
-
33
49
  ## [6.4.0] 2025-09-08
34
50
 
35
51
  - [hardis:project:deploy:smart](https://sfdx-hardis.cloudity.com/hardis/project/deploy/smart/): New beta feature **useDeltaDeploymentWithDependencies** to add dependencies to the delta deployment package.
@@ -48,13 +64,12 @@ Note: Can be used with `sfdx plugins:install sfdx-hardis@beta` and docker image
48
64
  - Set initPermissionSets config prop to array of strings
49
65
  - [hardis:org:diagnose:unsecure-connected-apps](https://sfdx-hardis.cloudity.com/hardis/org/diagnose/unsecure-connected-apps/): Handle case where OAuth Token App menu item is not found
50
66
 
51
-
52
67
  ## [6.3.1] 2025-09-07
53
68
 
54
69
  - Update Grafana Home Dashboard to add Unsecure Connected Apps
55
70
  - Fix Auth configuration command for Dev Hub
56
71
  - Allow to use org shapes for scratch org creation with env variable **SCRATCH_ORG_SHAPE**
57
- - Replace `my.salesforce-setup.com` by `my.salesforce.com` when prompting instance URL
72
+ - Replace `my.salesforce-setup.com` by `my.salesforce.com` when prompting instance URL
58
73
 
59
74
  ## [6.3.0] 2025-09-06
60
75
 
@@ -20,7 +20,7 @@ stages:
20
20
 
21
21
  # Jobs are run on sfdx-hardis image, that includes all required dependencies.
22
22
  # You can use latest, beta or latest-recommended
23
- image: hardisgroupcom/sfdx-hardis:latest
23
+ image: hardisgroupcom/sfdx-hardis:latest # If rate limits reached, use ghcr.io/hardisgroupcom/sfdx-hardis:latest
24
24
 
25
25
  # Force color for output logs for better readability
26
26
  variables:
@@ -54,7 +54,8 @@ pipeline {
54
54
  stage('Validation') {
55
55
  agent {
56
56
  docker {
57
- image 'hardisgroupcom/sfdx-hardis:latest'
57
+ // If rate limits reached, use ghcr.io/hardisgroupcom/sfdx-hardis:latest
58
+ image 'hardisgroupcom/sfdx-hardis:latest'
58
59
  }
59
60
  }
60
61
  when { changeRequest() }
@@ -74,6 +75,7 @@ pipeline {
74
75
  stage('Deployment') {
75
76
  agent {
76
77
  docker {
78
+ // If rate limits reached, use ghcr.io/hardisgroupcom/sfdx-hardis:latest
77
79
  image 'hardisgroupcom/sfdx-hardis:latest'
78
80
  }
79
81
  }
@@ -7,7 +7,7 @@ stages:
7
7
 
8
8
  # On execute les jobs sur l'image hardisgroupcom/sfdx-hardis qui contient les applications necessaires
9
9
  # Version latest recommandée, cependant beta et alpha peuvent être utilisées pour les tests
10
- image: hardisgroupcom/sfdx-hardis:latest
10
+ image: hardisgroupcom/sfdx-hardis:latest # If rate limits reached, use ghcr.io/hardisgroupcom/sfdx-hardis:latest
11
11
 
12
12
  # Variables globales aux jobs
13
13
  variables:
@@ -16,7 +16,7 @@ stages:
16
16
  - monitor
17
17
 
18
18
  # Use sfdx-hardis docker image to always be up to date with latest version
19
- image: hardisgroupcom/sfdx-hardis:latest # test with alpha
19
+ image: hardisgroupcom/sfdx-hardis:latest # If rate limits reached, use ghcr.io/hardisgroupcom/sfdx-hardis:latest
20
20
 
21
21
  ##############################################
22
22
  ### Sfdx Sources Backup + Push new commit ####
@@ -261,7 +261,7 @@ ${this.htmlInstructions}
261
261
  await fs.writeFile(path.join(this.outputMarkdownRoot, "manifests.md"), getMetaHideLines() + packageLines.join("\n") + `\n${this.footer}\n`);
262
262
  this.tempDir = await createTempDir();
263
263
  // Convert source to metadata API format to build prompts
264
- uxLog("action", this, c.cyan("Converting source to metadata API format..."));
264
+ uxLog("action", this, c.cyan("Converting source to metadata API format to ease the build of LLM prompts..."));
265
265
  await execCommand(`sf project convert source --metadata CustomObject --output-dir ${this.tempDir}`, this, { fail: true, output: true, debug: this.debugMode });
266
266
  this.objectFiles = (await glob("**/*.object", { cwd: this.tempDir, ignore: GLOB_IGNORE_PATTERNS }));
267
267
  sortCrossPlatform(this.objectFiles);
@@ -351,7 +351,7 @@ ${Project2Markdown.htmlInstructions}
351
351
  return { outputPackageXmlMarkdownFiles: this.outputPackageXmlMarkdownFiles };
352
352
  }
353
353
  async generateApexDocumentation() {
354
- uxLog("action", this, c.cyan("Generating Apex documentation... (if you don't want it, define GENERATE_APEX_DOC=false in your environment variables)"));
354
+ uxLog("action", this, c.cyan("Calling ApexDocGen to initialize Apex documentation... (if you don't want it, define GENERATE_APEX_DOC=false in your environment variables)"));
355
355
  const tempDir = await createTempDir();
356
356
  uxLog("log", this, c.grey(`Using temp directory ${tempDir}`));
357
357
  const packageDirs = this.project?.getPackageDirectories() || [];
@@ -396,7 +396,13 @@ ${Project2Markdown.htmlInstructions}
396
396
  });
397
397
  }
398
398
  // Complete generated documentation
399
+ if (apexFiles.length === 0) {
400
+ uxLog("log", this, c.yellow("No Apex class found in the project"));
401
+ return;
402
+ }
399
403
  const apexForMenu = { "All Apex Classes": "apex/index.md" };
404
+ WebSocketClient.sendProgressStartMessage("Generating Apex documentation...", apexFiles.length);
405
+ let counter = 0;
400
406
  for (const apexFile of apexFiles) {
401
407
  const apexName = path.basename(apexFile, ".cls").replace(".trigger", "");
402
408
  const apexContent = await fs.readFile(apexFile, "utf8");
@@ -431,7 +437,10 @@ ${Project2Markdown.htmlInstructions}
431
437
  await generatePdfFileFromMarkdown(mdFile);
432
438
  }
433
439
  }
440
+ counter++;
441
+ WebSocketClient.sendProgressStepMessage(counter, apexFiles.length);
434
442
  }
443
+ WebSocketClient.sendProgressEndMessage();
435
444
  this.addNavNode("Apex", apexForMenu);
436
445
  // Write index file for apex folder
437
446
  await fs.ensureDir(path.join(this.outputMarkdownRoot, "apex"));
@@ -439,7 +448,6 @@ ${Project2Markdown.htmlInstructions}
439
448
  await fs.writeFile(apexIndexFile, getMetaHideLines() + DocBuilderApex.buildIndexTable('', this.apexDescriptions).join("\n") + `\n\n${this.footer}\n`);
440
449
  }
441
450
  async generatePackagesDocumentation() {
442
- uxLog("action", this, c.cyan("Generating Installed Packages documentation..."));
443
451
  const packagesForMenu = { "All Packages": "packages/index.md" };
444
452
  // List packages
445
453
  const packages = this.sfdxHardisConfig.installedPackages || []; // CI/CD context
@@ -456,6 +464,8 @@ ${Project2Markdown.htmlInstructions}
456
464
  packages.push(pckg);
457
465
  }
458
466
  }
467
+ WebSocketClient.sendProgressStartMessage("Generating Installed Packages documentation...", packages.length);
468
+ let counter = 0;
459
469
  // Process packages
460
470
  for (const pckg of packages) {
461
471
  const packageName = pckg.SubscriberPackageName;
@@ -491,17 +501,22 @@ ${Project2Markdown.htmlInstructions}
491
501
  if (mdFileBad !== mdFile && fs.existsSync(mdFileBad)) {
492
502
  await fs.remove(mdFileBad);
493
503
  }
504
+ counter++;
505
+ WebSocketClient.sendProgressStepMessage(counter, packages.length);
494
506
  }
495
507
  this.addNavNode("Packages", packagesForMenu);
496
508
  // Write index file for packages folder
497
509
  await fs.ensureDir(path.join(this.outputMarkdownRoot, "packages"));
498
510
  const packagesIndexFile = path.join(this.outputMarkdownRoot, "packages", "index.md");
499
511
  await fs.writeFile(packagesIndexFile, getMetaHideLines() + DocBuilderPackage.buildIndexTable('', this.packageDescriptions).join("\n") + `\n\n${this.footer}\n`);
512
+ WebSocketClient.sendProgressEndMessage();
500
513
  }
501
514
  async generatePagesDocumentation() {
502
515
  const packageDirs = this.project?.getPackageDirectories() || [];
503
516
  const pageFiles = await listPageFiles(packageDirs);
504
517
  const pagesForMenu = { "All Lightning pages": "pages/index.md" };
518
+ WebSocketClient.sendProgressStartMessage("Generating Lightning Pages documentation...", pageFiles.length);
519
+ let counter = 0;
505
520
  for (const pagefile of pageFiles) {
506
521
  const pageName = path.basename(pagefile, ".flexipage-meta.xml");
507
522
  const mdFile = path.join(this.outputMarkdownRoot, "pages", pageName + ".md");
@@ -518,7 +533,10 @@ ${Project2Markdown.htmlInstructions}
518
533
  if (this.withPdf) {
519
534
  await generatePdfFileFromMarkdown(mdFile);
520
535
  }
536
+ counter++;
537
+ WebSocketClient.sendProgressStepMessage(counter, pageFiles.length);
521
538
  }
539
+ WebSocketClient.sendProgressEndMessage();
522
540
  this.addNavNode("Lightning Pages", pagesForMenu);
523
541
  // Write index file for pages folder
524
542
  await fs.ensureDir(path.join(this.outputMarkdownRoot, "pages"));
@@ -526,10 +544,16 @@ ${Project2Markdown.htmlInstructions}
526
544
  await fs.writeFile(pagesIndexFile, getMetaHideLines() + DocBuilderPage.buildIndexTable('', this.pageDescriptions).join("\n") + `\n\n${this.footer}\n`);
527
545
  }
528
546
  async generateProfilesDocumentation() {
529
- uxLog("action", this, c.cyan("Generating Profiles documentation... (if you don't want it, define GENERATE_PROFILES_DOC=false in your environment variables)"));
547
+ uxLog("action", this, c.cyan("Preparing generation of Profiles documentation... (if you don't want it, define GENERATE_PROFILES_DOC=false in your environment variables)"));
530
548
  const profilesForMenu = { "All Profiles": "profiles/index.md" };
531
549
  const profilesFiles = (await glob("**/profiles/**.profile-meta.xml", { cwd: process.cwd(), ignore: GLOB_IGNORE_PATTERNS }));
532
550
  sortCrossPlatform(profilesFiles);
551
+ if (profilesFiles.length === 0) {
552
+ uxLog("log", this, c.yellow("No profile found in the project"));
553
+ return;
554
+ }
555
+ WebSocketClient.sendProgressStartMessage("Generating Profiles documentation...", profilesFiles.length);
556
+ let counter = 0;
533
557
  for (const profileFile of profilesFiles) {
534
558
  const profileName = path.basename(profileFile, ".profile-meta.xml");
535
559
  const mdFile = path.join(this.outputMarkdownRoot, "profiles", profileName + ".md");
@@ -546,7 +570,10 @@ ${Project2Markdown.htmlInstructions}
546
570
  if (this.withPdf) {
547
571
  await generatePdfFileFromMarkdown(mdFile);
548
572
  }
573
+ counter++;
574
+ WebSocketClient.sendProgressStepMessage(counter, profilesFiles.length);
549
575
  }
576
+ WebSocketClient.sendProgressEndMessage();
550
577
  this.addNavNode("Profiles", profilesForMenu);
551
578
  // Write index file for profiles folder
552
579
  await fs.ensureDir(path.join(this.outputMarkdownRoot, "profiles"));
@@ -554,10 +581,16 @@ ${Project2Markdown.htmlInstructions}
554
581
  await fs.writeFile(profilesIndexFile, getMetaHideLines() + DocBuilderProfile.buildIndexTable('', this.profileDescriptions).join("\n") + `\n\n${this.footer}\n`);
555
582
  }
556
583
  async generatePermissionSetsDocumentation() {
557
- uxLog("action", this, c.cyan("Generating Permission Sets documentation... (if you don't want it, define GENERATE_PROFILES_DOC=false in your environment variables)"));
584
+ uxLog("action", this, c.cyan("Preparing generation of Permission Sets documentation... (if you don't want it, define GENERATE_PROFILES_DOC=false in your environment variables)"));
558
585
  const psForMenu = { "All Permission Sets": "permissionsets/index.md" };
559
586
  const psFiles = (await glob("**/permissionsets/**.permissionset-meta.xml", { cwd: process.cwd(), ignore: GLOB_IGNORE_PATTERNS }));
560
587
  sortCrossPlatform(psFiles);
588
+ if (psFiles.length === 0) {
589
+ uxLog("log", this, c.yellow("No permission set found in the project"));
590
+ return;
591
+ }
592
+ WebSocketClient.sendProgressStartMessage("Generating Permission Sets documentation...", psFiles.length);
593
+ let counter = 0;
561
594
  for (const psFile of psFiles) {
562
595
  const psName = path.basename(psFile, ".permissionset-meta.xml");
563
596
  const mdFile = path.join(this.outputMarkdownRoot, "permissionsets", psName + ".md");
@@ -577,7 +610,10 @@ ${Project2Markdown.htmlInstructions}
577
610
  if (this.withPdf) {
578
611
  await generatePdfFileFromMarkdown(mdFile);
579
612
  }
613
+ counter++;
614
+ WebSocketClient.sendProgressStepMessage(counter, psFiles.length);
580
615
  }
616
+ WebSocketClient.sendProgressEndMessage();
581
617
  this.addNavNode("Permission Sets", psForMenu);
582
618
  // Write index file for permission sets folder
583
619
  await fs.ensureDir(path.join(this.outputMarkdownRoot, "permissionsets"));
@@ -585,10 +621,16 @@ ${Project2Markdown.htmlInstructions}
585
621
  await fs.writeFile(psIndexFile, getMetaHideLines() + DocBuilderPermissionSet.buildIndexTable('', this.permissionSetsDescriptions).join("\n") + `\n\n${this.footer}\n`);
586
622
  }
587
623
  async generatePermissionSetGroupsDocumentation() {
588
- uxLog("action", this, c.cyan("Generating Permission Set Groups documentation..."));
624
+ uxLog("action", this, c.cyan("Preparing generation of Permission Set Groups documentation..."));
589
625
  const psgForMenu = { "All Permission Set Groups": "permissionsetgroups/index.md" };
590
626
  const psgFiles = (await glob("**/permissionsetgroups/**.permissionsetgroup-meta.xml", { cwd: process.cwd(), ignore: GLOB_IGNORE_PATTERNS }));
591
627
  sortCrossPlatform(psgFiles);
628
+ if (psgFiles.length === 0) {
629
+ uxLog("log", this, c.yellow("No permission set group found in the project"));
630
+ return;
631
+ }
632
+ WebSocketClient.sendProgressStartMessage("Generating Permission Set Groups documentation...", psgFiles.length);
633
+ let counter = 0;
592
634
  for (const psgFile of psgFiles) {
593
635
  const psgName = path.basename(psgFile, ".permissionsetgroup-meta.xml");
594
636
  const mdFile = path.join(this.outputMarkdownRoot, "permissionsetgroups", psgName + ".md");
@@ -608,7 +650,10 @@ ${Project2Markdown.htmlInstructions}
608
650
  if (this.withPdf) {
609
651
  await generatePdfFileFromMarkdown(mdFile);
610
652
  }
653
+ counter++;
654
+ WebSocketClient.sendProgressStepMessage(counter, psgFiles.length);
611
655
  }
656
+ WebSocketClient.sendProgressEndMessage();
612
657
  this.addNavNode("Permission Set Groups", psgForMenu);
613
658
  // Write index file for permission set groups folder
614
659
  await fs.ensureDir(path.join(this.outputMarkdownRoot, "permissionsetgroups"));
@@ -619,6 +664,10 @@ ${Project2Markdown.htmlInstructions}
619
664
  uxLog("action", this, c.cyan("Generating Roles documentation... (if you don't want it, define GENERATE_PROFILES_DOC=false in your environment variables)"));
620
665
  const roleFiles = (await glob("**/roles/**.role-meta.xml", { cwd: process.cwd(), ignore: GLOB_IGNORE_PATTERNS }));
621
666
  sortCrossPlatform(roleFiles);
667
+ if (roleFiles.length === 0) {
668
+ uxLog("log", this, c.yellow("No role found in the project"));
669
+ return;
670
+ }
622
671
  for (const roleFile of roleFiles) {
623
672
  const roleApiName = path.basename(roleFile, ".role-meta.xml");
624
673
  const roleXml = await fs.readFile(roleFile, "utf8");
@@ -639,7 +688,7 @@ ${Project2Markdown.htmlInstructions}
639
688
  }
640
689
  }
641
690
  async generateAssignmentRulesDocumentation() {
642
- uxLog("action", this, c.cyan("Generating Assignment Rules documentation... " +
691
+ uxLog("action", this, c.cyan("Preparing generation of Assignment Rules documentation... " +
643
692
  "(if you don't want it, define GENERATE_AUTOMATIONS_DOC=false in your environment variables)"));
644
693
  const assignmentRulesForMenu = { "All Assignment Rules": "assignmentRules/index.md" };
645
694
  const assignmentRulesFiles = (await glob("**/assignmentRules/**.assignmentRules-meta.xml", {
@@ -648,6 +697,23 @@ ${Project2Markdown.htmlInstructions}
648
697
  }));
649
698
  sortCrossPlatform(assignmentRulesFiles);
650
699
  const builder = new XMLBuilder();
700
+ // Count total rules for progress tracking
701
+ let totalRules = 0;
702
+ for (const assignmentRulesFile of assignmentRulesFiles) {
703
+ const assignmentRulesXml = await fs.readFile(assignmentRulesFile, "utf8");
704
+ const assignmentRulesXmlParsed = new XMLParser().parse(assignmentRulesXml);
705
+ let rulesList = assignmentRulesXmlParsed?.AssignmentRules?.assignmentRule || [];
706
+ if (!Array.isArray(rulesList)) {
707
+ rulesList = [rulesList];
708
+ }
709
+ totalRules += rulesList.length;
710
+ }
711
+ if (totalRules === 0) {
712
+ uxLog("log", this, c.yellow("No assignment rule found in the project"));
713
+ return;
714
+ }
715
+ WebSocketClient.sendProgressStartMessage("Generating Assignment Rules documentation...", totalRules);
716
+ let counter = 0;
651
717
  for (const assignmentRulesFile of assignmentRulesFiles) {
652
718
  const assignmentRulesXml = await fs.readFile(assignmentRulesFile, "utf8");
653
719
  const assignmentRulesXmlParsed = new XMLParser().parse(assignmentRulesXml);
@@ -670,15 +736,18 @@ ${Project2Markdown.htmlInstructions}
670
736
  if (this.withPdf) {
671
737
  await generatePdfFileFromMarkdown(mdFile);
672
738
  }
739
+ counter++;
740
+ WebSocketClient.sendProgressStepMessage(counter, totalRules);
673
741
  }
674
742
  }
743
+ WebSocketClient.sendProgressEndMessage();
675
744
  this.addNavNode("Assignment Rules", assignmentRulesForMenu);
676
745
  await fs.ensureDir(path.join(this.outputMarkdownRoot, "assignmentRules"));
677
746
  const psgIndexFile = path.join(this.outputMarkdownRoot, "assignmentRules", "index.md");
678
747
  await fs.writeFile(psgIndexFile, getMetaHideLines() + DocBuilderAssignmentRules.buildIndexTable('', this.assignmentRulesDescriptions).join("\n") + `\n${this.footer}\n`);
679
748
  }
680
749
  async generateApprovalProcessDocumentation() {
681
- uxLog("action", this, c.cyan("Generating Approval Processes documentation... " +
750
+ uxLog("action", this, c.cyan("Preparing generation of Approval Processes documentation... " +
682
751
  "(if you don't want it, define GENERATE_AUTOMATIONS_DOC=false in your environment variables)"));
683
752
  const approvalProcessesForMenu = { "All Approval Processes": "approvalProcesses/index.md" };
684
753
  const approvalProcessFiles = (await glob("**/approvalProcesses/**.approvalProcess-meta.xml", {
@@ -686,6 +755,12 @@ ${Project2Markdown.htmlInstructions}
686
755
  ignore: GLOB_IGNORE_PATTERNS
687
756
  }));
688
757
  sortCrossPlatform(approvalProcessFiles);
758
+ if (approvalProcessFiles.length === 0) {
759
+ uxLog("log", this, c.yellow("No approval process found in the project"));
760
+ return;
761
+ }
762
+ WebSocketClient.sendProgressStartMessage("Generating Approval Processes documentation...", approvalProcessFiles.length);
763
+ let counter = 0;
689
764
  for (const approvalProcessFile of approvalProcessFiles) {
690
765
  const approvalProcessName = path.basename(approvalProcessFile, ".approvalProcess-meta.xml");
691
766
  const mdFile = path.join(this.outputMarkdownRoot, "approvalProcesses", approvalProcessName + ".md");
@@ -701,14 +776,17 @@ ${Project2Markdown.htmlInstructions}
701
776
  if (this.withPdf) {
702
777
  await generatePdfFileFromMarkdown(mdFile);
703
778
  }
779
+ counter++;
780
+ WebSocketClient.sendProgressStepMessage(counter, approvalProcessFiles.length);
704
781
  }
782
+ WebSocketClient.sendProgressEndMessage();
705
783
  this.addNavNode("Approval Processes", approvalProcessesForMenu);
706
784
  await fs.ensureDir(path.join(this.outputMarkdownRoot, "approvalProcesses"));
707
785
  const approvalProcessesIndexFile = path.join(this.outputMarkdownRoot, "approvalProcesses", "index.md");
708
786
  await fs.writeFile(approvalProcessesIndexFile, getMetaHideLines() + DocBuilderApprovalProcess.buildIndexTable('', this.approvalProcessesDescriptions).join("\n") + `\n\n${this.footer}\n`);
709
787
  }
710
788
  async generateAutoResponseRulesDocumentation() {
711
- uxLog("action", this, c.cyan("Generating AutoResponse Rules documentation... " +
789
+ uxLog("action", this, c.cyan("Preparing generation of AutoResponse Rules documentation... " +
712
790
  "(if you don't want it, define GENERATE_AUTOMATIONS_DOC=false in your environment variables)"));
713
791
  const autoResponseRulesForMenu = { "All AutoResponse Rules": "autoResponseRules/index.md" };
714
792
  const autoResponseRulesFiles = (await glob("**/autoResponseRules/**.autoResponseRules-meta.xml", {
@@ -717,6 +795,23 @@ ${Project2Markdown.htmlInstructions}
717
795
  }));
718
796
  sortCrossPlatform(autoResponseRulesFiles);
719
797
  const builder = new XMLBuilder();
798
+ // Count total rules for progress tracking
799
+ let totalRules = 0;
800
+ for (const autoResponseRulesFile of autoResponseRulesFiles) {
801
+ const autoResponseRulesXml = await fs.readFile(autoResponseRulesFile, "utf8");
802
+ const autoResponseRulesXmlParsed = new XMLParser().parse(autoResponseRulesXml);
803
+ let rulesList = autoResponseRulesXmlParsed?.AutoResponseRules?.autoResponseRule || [];
804
+ if (!Array.isArray(rulesList)) {
805
+ rulesList = [rulesList];
806
+ }
807
+ totalRules += rulesList.length;
808
+ }
809
+ if (totalRules === 0) {
810
+ uxLog("log", this, c.yellow("No auto-response rules found in the project"));
811
+ return;
812
+ }
813
+ WebSocketClient.sendProgressStartMessage("Generating AutoResponse Rules documentation...", totalRules);
814
+ let counter = 0;
720
815
  for (const autoResponseRulesFile of autoResponseRulesFiles) {
721
816
  const autoResponseRulesXml = await fs.readFile(autoResponseRulesFile, "utf8");
722
817
  const autoResponseRulesXmlParsed = new XMLParser().parse(autoResponseRulesXml);
@@ -739,8 +834,11 @@ ${Project2Markdown.htmlInstructions}
739
834
  if (this.withPdf) {
740
835
  await generatePdfFileFromMarkdown(mdFile);
741
836
  }
837
+ counter++;
838
+ WebSocketClient.sendProgressStepMessage(counter, totalRules);
742
839
  }
743
840
  }
841
+ WebSocketClient.sendProgressEndMessage();
744
842
  this.addNavNode("AutoResponse Rules", autoResponseRulesForMenu);
745
843
  // Write index file for permission set groups folder
746
844
  await fs.ensureDir(path.join(this.outputMarkdownRoot, "autoResponseRules"));
@@ -748,7 +846,7 @@ ${Project2Markdown.htmlInstructions}
748
846
  await fs.writeFile(psgIndexFile, getMetaHideLines() + DocBuilderAutoResponseRules.buildIndexTable('', this.autoResponseRulesDescriptions).join("\n") + `\n${this.footer}\n`);
749
847
  }
750
848
  async generateEscalationRulesDocumentation() {
751
- uxLog("action", this, c.cyan("Generating Escalation Rules documentation... " +
849
+ uxLog("action", this, c.cyan("Preparing generation of Escalation Rules documentation... " +
752
850
  "(if you don't want it, define GENERATE_AUTOMATIONS_DOC=false in your environment variables)"));
753
851
  const escalationRulesForMenu = { "All Escalation Rules": "escalationRules/index.md" };
754
852
  const escalationRulesFiles = (await glob("**/escalationRules/**.escalationRules-meta.xml", {
@@ -757,6 +855,23 @@ ${Project2Markdown.htmlInstructions}
757
855
  }));
758
856
  sortCrossPlatform(escalationRulesFiles);
759
857
  const builder = new XMLBuilder();
858
+ // Count total rules for progress tracking
859
+ let totalRules = 0;
860
+ for (const escalationRulesFile of escalationRulesFiles) {
861
+ const escalationRulesXml = await fs.readFile(escalationRulesFile, "utf8");
862
+ const escalationRulesXmlParsed = new XMLParser().parse(escalationRulesXml);
863
+ let rulesList = escalationRulesXmlParsed?.EscalationRules?.escalationRule || [];
864
+ if (!Array.isArray(rulesList)) {
865
+ rulesList = [rulesList];
866
+ }
867
+ totalRules += rulesList.length;
868
+ }
869
+ if (totalRules === 0) {
870
+ uxLog("log", this, c.yellow("No escalation rules found in the project"));
871
+ return;
872
+ }
873
+ WebSocketClient.sendProgressStartMessage("Generating Escalation Rules documentation...", totalRules);
874
+ let counter = 0;
760
875
  for (const escalationRulesFile of escalationRulesFiles) {
761
876
  const escalationRulesXml = await fs.readFile(escalationRulesFile, "utf8");
762
877
  const escalationRulesXmlParsed = new XMLParser().parse(escalationRulesXml);
@@ -767,6 +882,8 @@ ${Project2Markdown.htmlInstructions}
767
882
  rulesList = [rulesList];
768
883
  }
769
884
  for (const rule of rulesList) {
885
+ counter++;
886
+ WebSocketClient.sendProgressStepMessage(counter);
770
887
  const currentRuleName = escalationRulesName + "." + rule?.fullName;
771
888
  escalationRulesForMenu[currentRuleName] = "escalationRules/" + currentRuleName + ".md";
772
889
  const mdFile = path.join(this.outputMarkdownRoot, "escalationRules", currentRuleName + ".md");
@@ -781,6 +898,7 @@ ${Project2Markdown.htmlInstructions}
781
898
  }
782
899
  }
783
900
  }
901
+ WebSocketClient.sendProgressEndMessage();
784
902
  this.addNavNode("Escalation Rules", escalationRulesForMenu);
785
903
  await fs.ensureDir(path.join(this.outputMarkdownRoot, "escalationRules"));
786
904
  const psgIndexFile = path.join(this.outputMarkdownRoot, "escalationRules", "index.md");
@@ -924,14 +1042,18 @@ ${Project2Markdown.htmlInstructions}
924
1042
  uxLog("action", this, c.cyan(`To generate a HTML WebSite with this documentation with a single command, see instructions at ${CONSTANTS.DOC_URL_ROOT}/hardis/doc/project2markdown/`));
925
1043
  }
926
1044
  async generateObjectsDocumentation() {
927
- uxLog("action", this, c.cyan("Generating Objects AI documentation... (if you don't want it, define GENERATE_OBJECTS_DOC=false in your environment variables)"));
1045
+ uxLog("action", this, c.cyan("Preparing generation of Objects AI documentation... (if you don't want it, define GENERATE_OBJECTS_DOC=false in your environment variables)"));
928
1046
  const objectLinksInfo = await this.generateLinksInfo();
929
1047
  const objectsForMenu = { "All objects": "objects/index.md" };
930
1048
  await fs.ensureDir(path.join(this.outputMarkdownRoot, "objects"));
1049
+ WebSocketClient.sendProgressStartMessage("Generating Objects documentation...", this.objectFiles.length);
1050
+ let counter = 0;
931
1051
  for (const objectFile of this.objectFiles) {
932
1052
  const objectName = path.basename(objectFile, ".object");
933
1053
  if ((objectName.endsWith("__dlm") || objectName.endsWith("__dll")) && !(process.env?.INCLUDE_DATA_CLOUD_DOC === "true")) {
934
1054
  uxLog("log", this, c.grey(`Skip Data Cloud Object ${objectName}... (use INCLUDE_DATA_CLOUD_DOC=true to enforce it)`));
1055
+ counter++;
1056
+ WebSocketClient.sendProgressStepMessage(counter, this.objectFiles.length);
935
1057
  continue;
936
1058
  }
937
1059
  uxLog("log", this, c.grey(`Generating markdown for Object ${objectName}...`));
@@ -989,7 +1111,10 @@ ${Project2Markdown.htmlInstructions}
989
1111
  if (this.withPdf) {
990
1112
  await generatePdfFileFromMarkdown(objectMdFile);
991
1113
  }
1114
+ counter++;
1115
+ WebSocketClient.sendProgressStepMessage(counter, this.objectFiles.length);
992
1116
  }
1117
+ WebSocketClient.sendProgressEndMessage();
993
1118
  this.addNavNode("Objects", objectsForMenu);
994
1119
  // Write index file for objects folder
995
1120
  await fs.ensureDir(path.join(this.outputMarkdownRoot, "objects"));
@@ -1005,7 +1130,7 @@ ${Project2Markdown.htmlInstructions}
1005
1130
  await replaceInFile(objectMdFile, '<!-- Attributes tables -->', attributesMarkdown);
1006
1131
  }
1007
1132
  async generateLinksInfo() {
1008
- uxLog("action", this, c.cyan("Generate MasterDetail and Lookup infos to provide context to AI prompt"));
1133
+ uxLog("log", this, c.cyan("Generate MasterDetail and Lookup infos to provide context to AI prompt"));
1009
1134
  const findFieldsPattern = `**/objects/**/fields/**.field-meta.xml`;
1010
1135
  const matchingFieldFiles = (await glob(findFieldsPattern, { cwd: process.cwd(), ignore: GLOB_IGNORE_PATTERNS })).map(file => file.replace(/\\/g, '/'));
1011
1136
  const customFieldsLinks = [];
@@ -1022,7 +1147,7 @@ ${Project2Markdown.htmlInstructions}
1022
1147
  return customFieldsLinks.join("\n") + "\n";
1023
1148
  }
1024
1149
  async generateFlowsDocumentation() {
1025
- uxLog("action", this, c.cyan("Generating Flows Visual documentation... (if you don't want it, define GENERATE_FLOW_DOC=false in your environment variables)"));
1150
+ uxLog("action", this, c.cyan("Preparing generation of Flows Visual documentation... (if you don't want it, define GENERATE_FLOW_DOC=false in your environment variables)"));
1026
1151
  const flowsForMenu = { "All flows": "flows/index.md" };
1027
1152
  await fs.ensureDir(path.join(this.outputMarkdownRoot, "flows"));
1028
1153
  const packageDirs = this.project?.getPackageDirectories();
@@ -1043,7 +1168,13 @@ ${Project2Markdown.htmlInstructions}
1043
1168
  const extractedNames = [...flowXml.matchAll(regex)].map(match => match[1]);
1044
1169
  flowDeps[flowName] = extractedNames;
1045
1170
  }
1171
+ if (flowFiles.length === 0) {
1172
+ uxLog("log", this, c.yellow("No flow found in the project"));
1173
+ return;
1174
+ }
1046
1175
  // Generate Flows documentation
1176
+ WebSocketClient.sendProgressStartMessage("Generating Flows documentation...", flowFiles.length);
1177
+ let counter = 0;
1047
1178
  for (const flowFile of flowFiles) {
1048
1179
  const flowName = path.basename(flowFile, ".flow-meta.xml");
1049
1180
  const flowXml = (await fs.readFile(flowFile, "utf8")).toString();
@@ -1059,12 +1190,16 @@ ${Project2Markdown.htmlInstructions}
1059
1190
  const outputFlowMdFile = path.join(this.outputMarkdownRoot, "flows", flowName + ".md");
1060
1191
  if (this.diffOnly && !updatedFlowNames.includes(flowName) && fs.existsSync(outputFlowMdFile)) {
1061
1192
  flowSkips.push(flowFile);
1193
+ counter++;
1194
+ WebSocketClient.sendProgressStepMessage(counter, flowFiles.length);
1062
1195
  continue;
1063
1196
  }
1064
1197
  uxLog("log", this, c.grey(`Generating markdown for Flow ${flowFile}...`));
1065
1198
  const genRes = await generateFlowMarkdownFile(flowName, flowXml, outputFlowMdFile, { collapsedDetails: false, describeWithAi: true, flowDependencies: flowDeps });
1066
1199
  if (!genRes) {
1067
1200
  flowErrors.push(flowFile);
1201
+ counter++;
1202
+ WebSocketClient.sendProgressStepMessage(counter, flowFiles.length);
1068
1203
  continue;
1069
1204
  }
1070
1205
  if (this.debugMode) {
@@ -1073,17 +1208,25 @@ ${Project2Markdown.htmlInstructions}
1073
1208
  const gen2res = await generateMarkdownFileWithMermaid(outputFlowMdFile, outputFlowMdFile, null, this.withPdf);
1074
1209
  if (!gen2res) {
1075
1210
  flowWarnings.push(flowFile);
1211
+ counter++;
1212
+ WebSocketClient.sendProgressStepMessage(counter, flowFiles.length);
1076
1213
  continue;
1077
1214
  }
1215
+ counter++;
1216
+ WebSocketClient.sendProgressStepMessage(counter, flowFiles.length);
1078
1217
  }
1218
+ WebSocketClient.sendProgressEndMessage();
1079
1219
  this.flowDescriptions = sortArray(this.flowDescriptions, { by: ['object', 'name'], order: ['asc', 'asc'] });
1080
1220
  // History
1081
1221
  if (this.withHistory) {
1082
- uxLog("action", this, c.cyan("Generating Flows Visual Git Diff History documentation..."));
1222
+ WebSocketClient.sendProgressStartMessage("Generating Flows History documentation...", flowFiles.length);
1223
+ let counter1 = 0;
1083
1224
  for (const flowFile of flowFiles) {
1084
1225
  const flowName = path.basename(flowFile, ".flow-meta.xml");
1085
1226
  const diffMdFile = path.join("docs", "flows", path.basename(flowFile).replace(".flow-meta.xml", "-history.md"));
1086
1227
  if (this.diffOnly && !updatedFlowNames.includes(flowName) && fs.existsSync(diffMdFile)) {
1228
+ counter1++;
1229
+ WebSocketClient.sendProgressStepMessage(counter1, flowFiles.length);
1087
1230
  continue;
1088
1231
  }
1089
1232
  try {
@@ -1092,7 +1235,10 @@ ${Project2Markdown.htmlInstructions}
1092
1235
  catch (e) {
1093
1236
  uxLog("warning", this, c.yellow(`Error generating history diff markdown: ${e.message}`));
1094
1237
  }
1238
+ counter1++;
1239
+ WebSocketClient.sendProgressStepMessage(counter1, flowFiles.length);
1095
1240
  }
1241
+ WebSocketClient.sendProgressEndMessage();
1096
1242
  }
1097
1243
  // Summary
1098
1244
  if (flowSkips.length > 0) {
@@ -1217,11 +1363,26 @@ ${Project2Markdown.htmlInstructions}
1217
1363
  }
1218
1364
  }
1219
1365
  async generateLwcDocumentation() {
1220
- uxLog("action", this, c.cyan("Generating Lightning Web Components documentation... " +
1366
+ uxLog("action", this, c.cyan("Preparing generation of Lightning Web Components documentation... " +
1221
1367
  "(if you don't want it, define GENERATE_LWC_DOC=false in your environment variables)"));
1222
1368
  const lwcForMenu = { "All Lightning Web Components": "lwc/index.md" };
1223
1369
  await fs.ensureDir(path.join(this.outputMarkdownRoot, "lwc"));
1224
1370
  const packageDirs = this.project?.getPackageDirectories() || [];
1371
+ // Count total LWC components for progress tracking
1372
+ let totalLwcComponents = 0;
1373
+ for (const packageDir of packageDirs) {
1374
+ const lwcMetaFiles = await glob(`${packageDir.path}/**/lwc/**/*.js-meta.xml`, {
1375
+ cwd: process.cwd(),
1376
+ ignore: GLOB_IGNORE_PATTERNS
1377
+ });
1378
+ totalLwcComponents += lwcMetaFiles.length;
1379
+ }
1380
+ if (totalLwcComponents === 0) {
1381
+ uxLog("log", this, c.yellow("No Lightning Web Component found in the project"));
1382
+ return;
1383
+ }
1384
+ WebSocketClient.sendProgressStartMessage("Generating Lightning Web Components documentation...", totalLwcComponents);
1385
+ let counter = 0;
1225
1386
  // Find all LWC components in all package directories
1226
1387
  for (const packageDir of packageDirs) {
1227
1388
  // Find LWC components (directories with .js-meta.xml files)
@@ -1230,6 +1391,8 @@ ${Project2Markdown.htmlInstructions}
1230
1391
  ignore: GLOB_IGNORE_PATTERNS
1231
1392
  });
1232
1393
  for (const lwcMetaFile of lwcMetaFiles) {
1394
+ counter++;
1395
+ WebSocketClient.sendProgressStepMessage(counter);
1233
1396
  const lwcDirPath = path.dirname(lwcMetaFile);
1234
1397
  const lwcName = path.basename(lwcDirPath);
1235
1398
  const mdFile = path.join(this.outputMarkdownRoot, "lwc", lwcName + ".md");
@@ -1274,6 +1437,7 @@ ${Project2Markdown.htmlInstructions}
1274
1437
  }
1275
1438
  }
1276
1439
  }
1440
+ WebSocketClient.sendProgressEndMessage();
1277
1441
  this.addNavNode("Lightning Web Components", lwcForMenu);
1278
1442
  // Write index file for LWC folder
1279
1443
  await fs.ensureDir(path.join(this.outputMarkdownRoot, "lwc"));