claude-code-templates 1.18.0 → 1.19.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-code-templates",
3
- "version": "1.18.0",
3
+ "version": "1.19.1",
4
4
  "description": "CLI tool to setup Claude Code configurations with framework-specific commands, automation hooks and MCP Servers for your projects",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -90,6 +90,6 @@
90
90
  ],
91
91
  "devDependencies": {
92
92
  "jest": "^30.0.4",
93
- "jest-watch-typeahead": "^2.2.2"
93
+ "jest-watch-typeahead": "^3.0.1"
94
94
  }
95
95
  }
package/src/index.js CHANGED
@@ -401,8 +401,11 @@ async function installIndividualAgent(agentName, targetDir, options) {
401
401
  source: 'github_main'
402
402
  });
403
403
 
404
+ return true;
405
+
404
406
  } catch (error) {
405
407
  console.log(chalk.red(`❌ Error installing agent: ${error.message}`));
408
+ return false;
406
409
  }
407
410
  }
408
411
 
@@ -464,8 +467,11 @@ async function installIndividualCommand(commandName, targetDir, options) {
464
467
  source: 'github_main'
465
468
  });
466
469
 
470
+ return true;
471
+
467
472
  } catch (error) {
468
473
  console.log(chalk.red(`❌ Error installing command: ${error.message}`));
474
+ return false;
469
475
  }
470
476
  }
471
477
 
@@ -547,8 +553,11 @@ async function installIndividualMCP(mcpName, targetDir, options) {
547
553
  source: 'github_main'
548
554
  });
549
555
 
556
+ return true;
557
+
550
558
  } catch (error) {
551
559
  console.log(chalk.red(`❌ Error installing MCP: ${error.message}`));
560
+ return false;
552
561
  }
553
562
  }
554
563
 
@@ -586,14 +595,14 @@ async function installIndividualSetting(settingName, targetDir, options) {
586
595
  delete settingConfig.description;
587
596
  }
588
597
 
589
- // Ask user where to install the setting (unless in silent mode)
590
- let settingsFile = 'settings.json'; // default
591
- if (!options.silent) {
598
+ // Use shared locations if provided (batch mode), otherwise ask user
599
+ let installLocations = options.sharedInstallLocations || ['local']; // default to local settings
600
+ if (!options.silent && !options.sharedInstallLocations) {
592
601
  const inquirer = require('inquirer');
593
- const { installLocation } = await inquirer.prompt([{
594
- type: 'list',
595
- name: 'installLocation',
596
- message: 'Where would you like to install this setting?',
602
+ const { selectedLocations } = await inquirer.prompt([{
603
+ type: 'checkbox',
604
+ name: 'selectedLocations',
605
+ message: 'Where would you like to install this setting? (Select one or more)',
597
606
  choices: [
598
607
  {
599
608
  name: '🏠 User settings (~/.claude/settings.json) - Applies to all projects',
@@ -605,19 +614,36 @@ async function installIndividualSetting(settingName, targetDir, options) {
605
614
  },
606
615
  {
607
616
  name: '⚙️ Local settings (.claude/settings.local.json) - Personal, not committed',
608
- value: 'local'
617
+ value: 'local',
618
+ checked: true // Default selection
609
619
  },
610
620
  {
611
621
  name: '🏢 Enterprise managed settings - System-wide policy (requires admin)',
612
622
  value: 'enterprise'
613
623
  }
614
624
  ],
615
- default: 'local'
625
+ validate: function(answer) {
626
+ if (answer.length < 1) {
627
+ return 'You must choose at least one installation location.';
628
+ }
629
+ return true;
630
+ }
616
631
  }]);
617
-
632
+
633
+ installLocations = selectedLocations;
634
+ }
635
+
636
+ // Install the setting in each selected location
637
+ let successfulInstallations = 0;
638
+ for (const installLocation of installLocations) {
639
+ console.log(chalk.blue(`\n📍 Installing "${settingName}" in ${installLocation} settings...`));
640
+
641
+ let currentTargetDir = targetDir;
642
+ let settingsFile = 'settings.local.json'; // default
643
+
618
644
  if (installLocation === 'user') {
619
645
  const os = require('os');
620
- targetDir = os.homedir();
646
+ currentTargetDir = os.homedir();
621
647
  settingsFile = 'settings.json';
622
648
  } else if (installLocation === 'project') {
623
649
  settingsFile = 'settings.json';
@@ -629,156 +655,177 @@ async function installIndividualSetting(settingName, targetDir, options) {
629
655
 
630
656
  if (platform === 'darwin') {
631
657
  // macOS
632
- targetDir = '/Library/Application Support/ClaudeCode';
658
+ currentTargetDir = '/Library/Application Support/ClaudeCode';
633
659
  settingsFile = 'managed-settings.json';
634
660
  } else if (platform === 'linux' || (process.platform === 'win32' && process.env.WSL_DISTRO_NAME)) {
635
661
  // Linux and WSL
636
- targetDir = '/etc/claude-code';
662
+ currentTargetDir = '/etc/claude-code';
637
663
  settingsFile = 'managed-settings.json';
638
664
  } else if (platform === 'win32') {
639
665
  // Windows
640
- targetDir = 'C:\\ProgramData\\ClaudeCode';
666
+ currentTargetDir = 'C:\\ProgramData\\ClaudeCode';
641
667
  settingsFile = 'managed-settings.json';
642
668
  } else {
643
669
  console.log(chalk.yellow('⚠️ Platform not supported for enterprise settings. Using user settings instead.'));
644
670
  const os = require('os');
645
- targetDir = os.homedir();
671
+ currentTargetDir = os.homedir();
646
672
  settingsFile = 'settings.json';
647
673
  }
648
674
 
649
675
  console.log(chalk.yellow(`⚠️ Enterprise settings require administrator privileges.`));
650
- console.log(chalk.gray(`📍 Target path: ${path.join(targetDir, settingsFile)}`));
676
+ console.log(chalk.gray(`📍 Target path: ${path.join(currentTargetDir, settingsFile)}`));
651
677
  }
652
- }
653
-
654
- // Determine target directory and file based on selection
655
- const claudeDir = path.join(targetDir, '.claude');
656
- const targetSettingsFile = path.join(claudeDir, settingsFile);
657
- let existingConfig = {};
658
-
659
- // For enterprise settings, create directory structure directly (not under .claude)
660
- if (settingsFile === 'managed-settings.json') {
661
- // Ensure enterprise directory exists (requires admin privileges)
662
- try {
663
- await fs.ensureDir(targetDir);
664
- } catch (error) {
665
- console.log(chalk.red(`❌ Failed to create enterprise directory: ${error.message}`));
666
- console.log(chalk.yellow('💡 Try running with administrator privileges or choose a different installation location.'));
667
- return;
668
- }
669
- } else {
670
- // Ensure .claude directory exists for regular settings
671
- await fs.ensureDir(claudeDir);
672
- }
673
-
674
- // Read existing configuration
675
- const actualTargetFile = settingsFile === 'managed-settings.json'
676
- ? path.join(targetDir, settingsFile)
677
- : targetSettingsFile;
678
678
 
679
- if (await fs.pathExists(actualTargetFile)) {
680
- existingConfig = await fs.readJson(actualTargetFile);
681
- console.log(chalk.yellow(`📝 Existing ${settingsFile} found, merging configurations...`));
682
- }
683
-
684
- // Check for conflicts before merging
685
- const conflicts = [];
686
-
687
- // Check for conflicting environment variables
688
- if (existingConfig.env && settingConfig.env) {
689
- Object.keys(settingConfig.env).forEach(key => {
690
- if (existingConfig.env[key] && existingConfig.env[key] !== settingConfig.env[key]) {
691
- conflicts.push(`Environment variable "${key}" (current: "${existingConfig.env[key]}", new: "${settingConfig.env[key]}")`);
679
+ // Determine target directory and file based on selection
680
+ const claudeDir = path.join(currentTargetDir, '.claude');
681
+ const targetSettingsFile = path.join(claudeDir, settingsFile);
682
+ let existingConfig = {};
683
+
684
+ // For enterprise settings, create directory structure directly (not under .claude)
685
+ if (settingsFile === 'managed-settings.json') {
686
+ // Ensure enterprise directory exists (requires admin privileges)
687
+ try {
688
+ await fs.ensureDir(currentTargetDir);
689
+ } catch (error) {
690
+ console.log(chalk.red(`❌ Failed to create enterprise directory: ${error.message}`));
691
+ console.log(chalk.yellow('💡 Try running with administrator privileges or choose a different installation location.'));
692
+ continue; // Skip this location and continue with others
692
693
  }
693
- });
694
- }
695
-
696
- // Check for conflicting top-level settings
697
- Object.keys(settingConfig).forEach(key => {
698
- if (key !== 'permissions' && key !== 'env' && key !== 'hooks' &&
699
- existingConfig[key] !== undefined && existingConfig[key] !== settingConfig[key]) {
700
- conflicts.push(`Setting "${key}" (current: "${existingConfig[key]}", new: "${settingConfig[key]}")`);
694
+ } else {
695
+ // Ensure .claude directory exists for regular settings
696
+ await fs.ensureDir(claudeDir);
701
697
  }
702
- });
703
-
704
- // Ask user about conflicts if any exist and not in silent mode
705
- if (conflicts.length > 0 && !options.silent) {
706
- console.log(chalk.yellow(`\n⚠️ Conflicts detected while installing setting "${settingName}":`));
707
- conflicts.forEach(conflict => console.log(chalk.gray(` • ${conflict}`)));
708
698
 
709
- const inquirer = require('inquirer');
710
- const { shouldOverwrite } = await inquirer.prompt([{
711
- type: 'confirm',
712
- name: 'shouldOverwrite',
713
- message: 'Do you want to overwrite the existing configuration?',
714
- default: false
715
- }]);
699
+ // Read existing configuration
700
+ const actualTargetFile = settingsFile === 'managed-settings.json'
701
+ ? path.join(currentTargetDir, settingsFile)
702
+ : targetSettingsFile;
703
+
704
+ if (await fs.pathExists(actualTargetFile)) {
705
+ existingConfig = await fs.readJson(actualTargetFile);
706
+ console.log(chalk.yellow(`📝 Existing ${settingsFile} found, merging configurations...`));
707
+ }
716
708
 
717
- if (!shouldOverwrite) {
718
- console.log(chalk.yellow(`⏹️ Installation of setting "${settingName}" cancelled by user.`));
719
- return;
709
+ // Check for conflicts before merging
710
+ const conflicts = [];
711
+
712
+ // Check for conflicting environment variables
713
+ if (existingConfig.env && settingConfig.env) {
714
+ Object.keys(settingConfig.env).forEach(key => {
715
+ if (existingConfig.env[key] && existingConfig.env[key] !== settingConfig.env[key]) {
716
+ conflicts.push(`Environment variable "${key}" (current: "${existingConfig.env[key]}", new: "${settingConfig.env[key]}")`);
717
+ }
718
+ });
720
719
  }
721
- } else if (conflicts.length > 0 && options.silent) {
722
- // In silent mode (batch installation), skip conflicting settings and warn
723
- console.log(chalk.yellow(`⚠️ Skipping setting "${settingName}" due to conflicts (use individual installation to resolve)`));
724
- return;
725
- }
726
-
727
- // Deep merge configurations
728
- const mergedConfig = {
729
- ...existingConfig,
730
- ...settingConfig
731
- };
732
-
733
- // Deep merge specific sections (only if no conflicts or user approved overwrite)
734
- if (existingConfig.permissions && settingConfig.permissions) {
735
- mergedConfig.permissions = {
736
- ...existingConfig.permissions,
737
- ...settingConfig.permissions
738
- };
739
720
 
740
- // Merge arrays for allow, deny, ask (no conflicts here, just merge)
741
- ['allow', 'deny', 'ask'].forEach(key => {
742
- if (existingConfig.permissions[key] && settingConfig.permissions[key]) {
743
- mergedConfig.permissions[key] = [
744
- ...new Set([...existingConfig.permissions[key], ...settingConfig.permissions[key]])
745
- ];
721
+ // Check for conflicting top-level settings
722
+ Object.keys(settingConfig).forEach(key => {
723
+ if (key !== 'permissions' && key !== 'env' && key !== 'hooks' &&
724
+ existingConfig[key] !== undefined && JSON.stringify(existingConfig[key]) !== JSON.stringify(settingConfig[key])) {
725
+
726
+ // For objects, just indicate the setting name without showing the complex values
727
+ if (typeof existingConfig[key] === 'object' && existingConfig[key] !== null &&
728
+ typeof settingConfig[key] === 'object' && settingConfig[key] !== null) {
729
+ conflicts.push(`Setting "${key}" (will be overwritten with new configuration)`);
730
+ } else {
731
+ conflicts.push(`Setting "${key}" (current: "${existingConfig[key]}", new: "${settingConfig[key]}")`);
732
+ }
746
733
  }
747
734
  });
748
- }
749
-
750
- if (existingConfig.env && settingConfig.env) {
751
- mergedConfig.env = {
752
- ...existingConfig.env,
753
- ...settingConfig.env
754
- };
755
- }
756
-
757
- if (existingConfig.hooks && settingConfig.hooks) {
758
- mergedConfig.hooks = {
759
- ...existingConfig.hooks,
760
- ...settingConfig.hooks
735
+
736
+ // Ask user about conflicts if any exist
737
+ if (conflicts.length > 0) {
738
+ console.log(chalk.yellow(`\n⚠️ Conflicts detected while installing setting "${settingName}" in ${installLocation}:`));
739
+ conflicts.forEach(conflict => console.log(chalk.gray(` • ${conflict}`)));
740
+
741
+ const inquirer = require('inquirer');
742
+ const { shouldOverwrite } = await inquirer.prompt([{
743
+ type: 'confirm',
744
+ name: 'shouldOverwrite',
745
+ message: `Do you want to overwrite the existing configuration in ${installLocation}?`,
746
+ default: false
747
+ }]);
748
+
749
+ if (!shouldOverwrite) {
750
+ console.log(chalk.yellow(`⏹️ Installation of setting "${settingName}" in ${installLocation} cancelled by user.`));
751
+ continue; // Skip this location and continue with others
752
+ }
753
+ }
754
+
755
+ // Deep merge configurations
756
+ const mergedConfig = {
757
+ ...existingConfig,
758
+ ...settingConfig
761
759
  };
760
+
761
+ // Deep merge specific sections (only if no conflicts or user approved overwrite)
762
+ if (existingConfig.permissions && settingConfig.permissions) {
763
+ mergedConfig.permissions = {
764
+ ...existingConfig.permissions,
765
+ ...settingConfig.permissions
766
+ };
767
+
768
+ // Merge arrays for allow, deny, ask (no conflicts here, just merge)
769
+ ['allow', 'deny', 'ask'].forEach(key => {
770
+ if (existingConfig.permissions[key] && settingConfig.permissions[key]) {
771
+ mergedConfig.permissions[key] = [
772
+ ...new Set([...existingConfig.permissions[key], ...settingConfig.permissions[key]])
773
+ ];
774
+ }
775
+ });
776
+ }
777
+
778
+ if (existingConfig.env && settingConfig.env) {
779
+ mergedConfig.env = {
780
+ ...existingConfig.env,
781
+ ...settingConfig.env
782
+ };
783
+ }
784
+
785
+ if (existingConfig.hooks && settingConfig.hooks) {
786
+ mergedConfig.hooks = {
787
+ ...existingConfig.hooks,
788
+ ...settingConfig.hooks
789
+ };
790
+ }
791
+
792
+ // Write the merged configuration
793
+ await fs.writeJson(actualTargetFile, mergedConfig, { spaces: 2 });
794
+
795
+ if (!options.silent) {
796
+ console.log(chalk.green(`✅ Setting "${settingName}" installed successfully in ${installLocation}!`));
797
+ console.log(chalk.cyan(`📁 Configuration merged into: ${actualTargetFile}`));
798
+ console.log(chalk.cyan(`📦 Downloaded from: ${githubUrl}`));
799
+ }
800
+
801
+ // Track successful setting installation for this location
802
+ trackingService.trackDownload('setting', settingName, {
803
+ installation_type: 'individual_setting',
804
+ installation_location: installLocation,
805
+ merged_with_existing: Object.keys(existingConfig).length > 0,
806
+ source: 'github_main'
807
+ });
808
+
809
+ // Increment successful installations counter
810
+ successfulInstallations++;
762
811
  }
763
812
 
764
- // Write the merged configuration
765
- await fs.writeJson(actualTargetFile, mergedConfig, { spaces: 2 });
766
-
813
+ // Summary after all installations
767
814
  if (!options.silent) {
768
- console.log(chalk.green(`✅ Setting "${settingName}" installed successfully!`));
769
- console.log(chalk.cyan(`📁 Configuration merged into: ${actualTargetFile}`));
770
- console.log(chalk.cyan(`📦 Downloaded from: ${githubUrl}`));
815
+ if (successfulInstallations === installLocations.length) {
816
+ console.log(chalk.green(`\n🎉 Setting "${settingName}" successfully installed in ${successfulInstallations} location(s)!`));
817
+ } else {
818
+ console.log(chalk.yellow(`\n⚠️ Setting "${settingName}" installed in ${successfulInstallations} of ${installLocations.length} location(s).`));
819
+ const failedCount = installLocations.length - successfulInstallations;
820
+ console.log(chalk.red(`❌ ${failedCount} installation(s) failed due to permission or other errors.`));
821
+ }
771
822
  }
772
823
 
773
- // Track successful setting installation
774
- trackingService.trackDownload('setting', settingName, {
775
- installation_type: 'individual_setting',
776
- merged_with_existing: Object.keys(existingConfig).length > 0,
777
- source: 'github_main'
778
- });
824
+ return successfulInstallations;
779
825
 
780
826
  } catch (error) {
781
827
  console.log(chalk.red(`❌ Error installing setting: ${error.message}`));
828
+ return 0;
782
829
  }
783
830
  }
784
831
 
@@ -816,14 +863,14 @@ async function installIndividualHook(hookName, targetDir, options) {
816
863
  delete hookConfig.description;
817
864
  }
818
865
 
819
- // Ask user where to install the hook (unless in silent mode)
820
- let settingsFile = 'settings.json'; // default
821
- if (!options.silent) {
866
+ // Use shared locations if provided (batch mode), otherwise ask user
867
+ let installLocations = options.sharedInstallLocations || ['local']; // default to local settings
868
+ if (!options.silent && !options.sharedInstallLocations) {
822
869
  const inquirer = require('inquirer');
823
- const { installLocation } = await inquirer.prompt([{
824
- type: 'list',
825
- name: 'installLocation',
826
- message: 'Where would you like to install this hook?',
870
+ const { selectedLocations } = await inquirer.prompt([{
871
+ type: 'checkbox',
872
+ name: 'selectedLocations',
873
+ message: 'Where would you like to install this hook? (Select one or more)',
827
874
  choices: [
828
875
  {
829
876
  name: '🏠 User settings (~/.claude/settings.json) - Applies to all projects',
@@ -835,19 +882,36 @@ async function installIndividualHook(hookName, targetDir, options) {
835
882
  },
836
883
  {
837
884
  name: '⚙️ Local settings (.claude/settings.local.json) - Personal, not committed',
838
- value: 'local'
885
+ value: 'local',
886
+ checked: true // Default selection
839
887
  },
840
888
  {
841
889
  name: '🏢 Enterprise managed settings - System-wide policy (requires admin)',
842
890
  value: 'enterprise'
843
891
  }
844
892
  ],
845
- default: 'local'
893
+ validate: function(answer) {
894
+ if (answer.length < 1) {
895
+ return 'You must choose at least one installation location.';
896
+ }
897
+ return true;
898
+ }
846
899
  }]);
900
+
901
+ installLocations = selectedLocations;
902
+ }
903
+
904
+ // Install the hook in each selected location
905
+ let successfulInstallations = 0;
906
+ for (const installLocation of installLocations) {
907
+ console.log(chalk.blue(`\n📍 Installing "${hookName}" in ${installLocation} settings...`));
908
+
909
+ let currentTargetDir = targetDir;
910
+ let settingsFile = 'settings.local.json'; // default
847
911
 
848
912
  if (installLocation === 'user') {
849
913
  const os = require('os');
850
- targetDir = os.homedir();
914
+ currentTargetDir = os.homedir();
851
915
  settingsFile = 'settings.json';
852
916
  } else if (installLocation === 'project') {
853
917
  settingsFile = 'settings.json';
@@ -859,151 +923,165 @@ async function installIndividualHook(hookName, targetDir, options) {
859
923
 
860
924
  if (platform === 'darwin') {
861
925
  // macOS
862
- targetDir = '/Library/Application Support/ClaudeCode';
926
+ currentTargetDir = '/Library/Application Support/ClaudeCode';
863
927
  settingsFile = 'managed-settings.json';
864
928
  } else if (platform === 'linux' || (process.platform === 'win32' && process.env.WSL_DISTRO_NAME)) {
865
929
  // Linux and WSL
866
- targetDir = '/etc/claude-code';
930
+ currentTargetDir = '/etc/claude-code';
867
931
  settingsFile = 'managed-settings.json';
868
932
  } else if (platform === 'win32') {
869
933
  // Windows
870
- targetDir = 'C:\\ProgramData\\ClaudeCode';
934
+ currentTargetDir = 'C:\\ProgramData\\ClaudeCode';
871
935
  settingsFile = 'managed-settings.json';
872
936
  } else {
873
937
  console.log(chalk.yellow('⚠️ Platform not supported for enterprise settings. Using user settings instead.'));
874
938
  const os = require('os');
875
- targetDir = os.homedir();
939
+ currentTargetDir = os.homedir();
876
940
  settingsFile = 'settings.json';
877
941
  }
878
942
 
879
943
  console.log(chalk.yellow(`⚠️ Enterprise settings require administrator privileges.`));
880
- console.log(chalk.gray(`📍 Target path: ${path.join(targetDir, settingsFile)}`));
944
+ console.log(chalk.gray(`📍 Target path: ${path.join(currentTargetDir, settingsFile)}`));
881
945
  }
882
- }
883
-
884
- // Determine target directory and file based on selection
885
- const claudeDir = path.join(targetDir, '.claude');
886
- const targetSettingsFile = path.join(claudeDir, settingsFile);
887
- let existingConfig = {};
888
-
889
- // For enterprise settings, create directory structure directly (not under .claude)
890
- if (settingsFile === 'managed-settings.json') {
891
- // Ensure enterprise directory exists (requires admin privileges)
892
- try {
893
- await fs.ensureDir(targetDir);
894
- } catch (error) {
895
- console.log(chalk.red(`❌ Failed to create enterprise directory: ${error.message}`));
896
- console.log(chalk.yellow('💡 Try running with administrator privileges or choose a different installation location.'));
897
- return;
946
+
947
+ // Determine target directory and file based on selection
948
+ const claudeDir = path.join(currentTargetDir, '.claude');
949
+ const targetSettingsFile = path.join(claudeDir, settingsFile);
950
+ let existingConfig = {};
951
+
952
+ // For enterprise settings, create directory structure directly (not under .claude)
953
+ if (settingsFile === 'managed-settings.json') {
954
+ // Ensure enterprise directory exists (requires admin privileges)
955
+ try {
956
+ await fs.ensureDir(currentTargetDir);
957
+ } catch (error) {
958
+ console.log(chalk.red(`❌ Failed to create enterprise directory: ${error.message}`));
959
+ console.log(chalk.yellow('💡 Try running with administrator privileges or choose a different installation location.'));
960
+ continue; // Skip this location and continue with others
961
+ }
962
+ } else {
963
+ // Ensure .claude directory exists for regular settings
964
+ await fs.ensureDir(claudeDir);
898
965
  }
899
- } else {
900
- // Ensure .claude directory exists for regular settings
901
- await fs.ensureDir(claudeDir);
902
- }
903
-
904
- // Read existing configuration
905
- const actualTargetFile = settingsFile === 'managed-settings.json'
906
- ? path.join(targetDir, settingsFile)
907
- : targetSettingsFile;
908
966
 
909
- if (await fs.pathExists(actualTargetFile)) {
910
- existingConfig = await fs.readJson(actualTargetFile);
911
- console.log(chalk.yellow(`📝 Existing ${settingsFile} found, merging hook configurations...`));
912
- }
913
-
914
- // Check for conflicts before merging (simplified for new array format)
915
- const conflicts = [];
916
-
917
- // For the new array format, we'll allow appending rather than conflict detection
918
- // This is because Claude Code's array format naturally supports multiple hooks
919
- // Conflicts are less likely and generally hooks can coexist
920
-
921
- // Ask user about conflicts if any exist and not in silent mode
922
- if (conflicts.length > 0 && !options.silent) {
923
- console.log(chalk.yellow(`\n⚠️ Conflicts detected while installing hook "${hookName}":`));
924
- conflicts.forEach(conflict => console.log(chalk.gray(` • ${conflict}`)));
967
+ // Read existing configuration
968
+ const actualTargetFile = settingsFile === 'managed-settings.json'
969
+ ? path.join(currentTargetDir, settingsFile)
970
+ : targetSettingsFile;
971
+
972
+ if (await fs.pathExists(actualTargetFile)) {
973
+ existingConfig = await fs.readJson(actualTargetFile);
974
+ console.log(chalk.yellow(`📝 Existing ${settingsFile} found, merging hook configurations...`));
975
+ }
925
976
 
926
- const inquirer = require('inquirer');
927
- const { shouldOverwrite } = await inquirer.prompt([{
928
- type: 'confirm',
929
- name: 'shouldOverwrite',
930
- message: 'Do you want to overwrite the existing hook configuration?',
931
- default: false
932
- }]);
977
+ // Check for conflicts before merging (simplified for new array format)
978
+ const conflicts = [];
933
979
 
934
- if (!shouldOverwrite) {
935
- console.log(chalk.yellow(`⏹️ Installation of hook "${hookName}" cancelled by user.`));
936
- return;
980
+ // For the new array format, we'll allow appending rather than conflict detection
981
+ // This is because Claude Code's array format naturally supports multiple hooks
982
+ // Conflicts are less likely and generally hooks can coexist
983
+
984
+ // Ask user about conflicts if any exist
985
+ if (conflicts.length > 0) {
986
+ console.log(chalk.yellow(`\n⚠️ Conflicts detected while installing hook "${hookName}" in ${installLocation}:`));
987
+ conflicts.forEach(conflict => console.log(chalk.gray(` • ${conflict}`)));
988
+
989
+ const inquirer = require('inquirer');
990
+ const { shouldOverwrite } = await inquirer.prompt([{
991
+ type: 'confirm',
992
+ name: 'shouldOverwrite',
993
+ message: `Do you want to overwrite the existing hook configuration in ${installLocation}?`,
994
+ default: false
995
+ }]);
996
+
997
+ if (!shouldOverwrite) {
998
+ console.log(chalk.yellow(`⏹️ Installation of hook "${hookName}" in ${installLocation} cancelled by user.`));
999
+ continue; // Skip this location and continue with others
1000
+ }
937
1001
  }
938
- } else if (conflicts.length > 0 && options.silent) {
939
- // In silent mode (batch installation), skip conflicting hooks and warn
940
- console.log(chalk.yellow(`⚠️ Skipping hook "${hookName}" due to conflicts (use individual installation to resolve)`));
941
- return;
942
- }
943
-
944
- // Deep merge configurations with proper hook array structure
945
- const mergedConfig = {
946
- ...existingConfig
947
- };
948
-
949
- // Initialize hooks structure if it doesn't exist
950
- if (!mergedConfig.hooks) {
951
- mergedConfig.hooks = {};
952
- }
953
-
954
- // Merge hook configurations properly (Claude Code expects arrays)
955
- if (hookConfig.hooks) {
956
- Object.keys(hookConfig.hooks).forEach(hookType => {
957
- if (!mergedConfig.hooks[hookType]) {
958
- // If hook type doesn't exist, just copy the array
959
- mergedConfig.hooks[hookType] = hookConfig.hooks[hookType];
960
- } else {
961
- // If hook type exists, append to the array (Claude Code format)
962
- if (Array.isArray(hookConfig.hooks[hookType])) {
963
- // New format: array of hook objects
964
- if (!Array.isArray(mergedConfig.hooks[hookType])) {
965
- // Convert old format to new format
966
- mergedConfig.hooks[hookType] = [];
967
- }
968
- // Append new hooks to existing array
969
- mergedConfig.hooks[hookType] = mergedConfig.hooks[hookType].concat(hookConfig.hooks[hookType]);
1002
+
1003
+ // Deep merge configurations with proper hook array structure
1004
+ const mergedConfig = {
1005
+ ...existingConfig
1006
+ };
1007
+
1008
+ // Initialize hooks structure if it doesn't exist
1009
+ if (!mergedConfig.hooks) {
1010
+ mergedConfig.hooks = {};
1011
+ }
1012
+
1013
+ // Merge hook configurations properly (Claude Code expects arrays)
1014
+ if (hookConfig.hooks) {
1015
+ Object.keys(hookConfig.hooks).forEach(hookType => {
1016
+ if (!mergedConfig.hooks[hookType]) {
1017
+ // If hook type doesn't exist, just copy the array
1018
+ mergedConfig.hooks[hookType] = hookConfig.hooks[hookType];
970
1019
  } else {
971
- // Old format compatibility: convert to new format
972
- console.log(chalk.yellow(`⚠️ Converting old hook format to new Claude Code format for ${hookType}`));
973
- if (!Array.isArray(mergedConfig.hooks[hookType])) {
974
- mergedConfig.hooks[hookType] = [];
1020
+ // If hook type exists, append to the array (Claude Code format)
1021
+ if (Array.isArray(hookConfig.hooks[hookType])) {
1022
+ // New format: array of hook objects
1023
+ if (!Array.isArray(mergedConfig.hooks[hookType])) {
1024
+ // Convert old format to new format
1025
+ mergedConfig.hooks[hookType] = [];
1026
+ }
1027
+ // Append new hooks to existing array
1028
+ mergedConfig.hooks[hookType] = mergedConfig.hooks[hookType].concat(hookConfig.hooks[hookType]);
1029
+ } else {
1030
+ // Old format compatibility: convert to new format
1031
+ console.log(chalk.yellow(`⚠️ Converting old hook format to new Claude Code format for ${hookType}`));
1032
+ if (!Array.isArray(mergedConfig.hooks[hookType])) {
1033
+ mergedConfig.hooks[hookType] = [];
1034
+ }
1035
+ // Add old format hook as a single matcher
1036
+ mergedConfig.hooks[hookType].push({
1037
+ matcher: "*",
1038
+ hooks: [{
1039
+ type: "command",
1040
+ command: hookConfig.hooks[hookType]
1041
+ }]
1042
+ });
975
1043
  }
976
- // Add old format hook as a single matcher
977
- mergedConfig.hooks[hookType].push({
978
- matcher: "*",
979
- hooks: [{
980
- type: "command",
981
- command: hookConfig.hooks[hookType]
982
- }]
983
- });
984
1044
  }
985
- }
1045
+ });
1046
+ }
1047
+
1048
+ // Write the merged configuration
1049
+ await fs.writeJson(actualTargetFile, mergedConfig, { spaces: 2 });
1050
+
1051
+ if (!options.silent) {
1052
+ console.log(chalk.green(`✅ Hook "${hookName}" installed successfully in ${installLocation}!`));
1053
+ console.log(chalk.cyan(`📁 Configuration merged into: ${actualTargetFile}`));
1054
+ console.log(chalk.cyan(`📦 Downloaded from: ${githubUrl}`));
1055
+ }
1056
+
1057
+ // Track successful hook installation for this location
1058
+ trackingService.trackDownload('hook', hookName, {
1059
+ installation_type: 'individual_hook',
1060
+ installation_location: installLocation,
1061
+ merged_with_existing: Object.keys(existingConfig).length > 0,
1062
+ source: 'github_main'
986
1063
  });
1064
+
1065
+ // Increment successful installations counter
1066
+ successfulInstallations++;
987
1067
  }
988
1068
 
989
- // Write the merged configuration
990
- await fs.writeJson(actualTargetFile, mergedConfig, { spaces: 2 });
991
-
1069
+ // Summary after all installations
992
1070
  if (!options.silent) {
993
- console.log(chalk.green(`✅ Hook "${hookName}" installed successfully!`));
994
- console.log(chalk.cyan(`📁 Configuration merged into: ${actualTargetFile}`));
995
- console.log(chalk.cyan(`📦 Downloaded from: ${githubUrl}`));
1071
+ if (successfulInstallations === installLocations.length) {
1072
+ console.log(chalk.green(`\n🎉 Hook "${hookName}" successfully installed in ${successfulInstallations} location(s)!`));
1073
+ } else {
1074
+ console.log(chalk.yellow(`\n⚠️ Hook "${hookName}" installed in ${successfulInstallations} of ${installLocations.length} location(s).`));
1075
+ const failedCount = installLocations.length - successfulInstallations;
1076
+ console.log(chalk.red(`❌ ${failedCount} installation(s) failed due to permission or other errors.`));
1077
+ }
996
1078
  }
997
1079
 
998
- // Track successful hook installation
999
- trackingService.trackDownload('hook', hookName, {
1000
- installation_type: 'individual_hook',
1001
- merged_with_existing: Object.keys(existingConfig).length > 0,
1002
- source: 'github_main'
1003
- });
1080
+ return successfulInstallations;
1004
1081
 
1005
1082
  } catch (error) {
1006
1083
  console.log(chalk.red(`❌ Error installing hook: ${error.message}`));
1084
+ return 0;
1007
1085
  }
1008
1086
  }
1009
1087
 
@@ -1144,34 +1222,92 @@ async function installMultipleComponents(options, targetDir) {
1144
1222
  console.log(chalk.gray(` Settings: ${components.settings.length}`));
1145
1223
  console.log(chalk.gray(` Hooks: ${components.hooks.length}`));
1146
1224
 
1225
+ // Counter for successfully installed components
1226
+ let successfullyInstalled = 0;
1227
+
1228
+ // Ask for installation locations once for configuration components (if any exist and not in silent mode)
1229
+ let sharedInstallLocations = ['local']; // default
1230
+ const hasSettingsOrHooks = components.settings.length > 0 || components.hooks.length > 0;
1231
+
1232
+ if (hasSettingsOrHooks && !options.yes) {
1233
+ console.log(chalk.blue('\n📍 Choose installation locations for configuration components:'));
1234
+ const inquirer = require('inquirer');
1235
+ const { selectedLocations } = await inquirer.prompt([{
1236
+ type: 'checkbox',
1237
+ name: 'selectedLocations',
1238
+ message: 'Where would you like to install the configuration components? (Select one or more)',
1239
+ choices: [
1240
+ {
1241
+ name: '🏠 User settings (~/.claude/settings.json) - Applies to all projects',
1242
+ value: 'user'
1243
+ },
1244
+ {
1245
+ name: '📁 Project settings (.claude/settings.json) - Shared with team',
1246
+ value: 'project'
1247
+ },
1248
+ {
1249
+ name: '⚙️ Local settings (.claude/settings.local.json) - Personal, not committed',
1250
+ value: 'local',
1251
+ checked: true // Default selection
1252
+ },
1253
+ {
1254
+ name: '🏢 Enterprise managed settings - System-wide policy (requires admin)',
1255
+ value: 'enterprise'
1256
+ }
1257
+ ],
1258
+ validate: function(answer) {
1259
+ if (answer.length < 1) {
1260
+ return 'You must choose at least one installation location.';
1261
+ }
1262
+ return true;
1263
+ }
1264
+ }]);
1265
+
1266
+ sharedInstallLocations = selectedLocations;
1267
+ console.log(chalk.cyan(`📋 Will install configuration components in: ${sharedInstallLocations.join(', ')}`));
1268
+ }
1269
+
1147
1270
  // Install agents
1148
1271
  for (const agent of components.agents) {
1149
1272
  console.log(chalk.gray(` Installing agent: ${agent}`));
1150
- await installIndividualAgent(agent, targetDir, { ...options, silent: true });
1273
+ const agentSuccess = await installIndividualAgent(agent, targetDir, { ...options, silent: true });
1274
+ if (agentSuccess) successfullyInstalled++;
1151
1275
  }
1152
1276
 
1153
1277
  // Install commands
1154
1278
  for (const command of components.commands) {
1155
1279
  console.log(chalk.gray(` Installing command: ${command}`));
1156
- await installIndividualCommand(command, targetDir, { ...options, silent: true });
1280
+ const commandSuccess = await installIndividualCommand(command, targetDir, { ...options, silent: true });
1281
+ if (commandSuccess) successfullyInstalled++;
1157
1282
  }
1158
1283
 
1159
1284
  // Install MCPs
1160
1285
  for (const mcp of components.mcps) {
1161
1286
  console.log(chalk.gray(` Installing MCP: ${mcp}`));
1162
- await installIndividualMCP(mcp, targetDir, { ...options, silent: true });
1287
+ const mcpSuccess = await installIndividualMCP(mcp, targetDir, { ...options, silent: true });
1288
+ if (mcpSuccess) successfullyInstalled++;
1163
1289
  }
1164
1290
 
1165
- // Install settings
1291
+ // Install settings (using shared installation locations)
1166
1292
  for (const setting of components.settings) {
1167
1293
  console.log(chalk.gray(` Installing setting: ${setting}`));
1168
- await installIndividualSetting(setting, targetDir, { ...options, silent: true });
1294
+ const settingSuccess = await installIndividualSetting(setting, targetDir, {
1295
+ ...options,
1296
+ silent: true,
1297
+ sharedInstallLocations: sharedInstallLocations
1298
+ });
1299
+ if (settingSuccess > 0) successfullyInstalled++;
1169
1300
  }
1170
1301
 
1171
- // Install hooks
1302
+ // Install hooks (using shared installation locations)
1172
1303
  for (const hook of components.hooks) {
1173
1304
  console.log(chalk.gray(` Installing hook: ${hook}`));
1174
- await installIndividualHook(hook, targetDir, { ...options, silent: true });
1305
+ const hookSuccess = await installIndividualHook(hook, targetDir, {
1306
+ ...options,
1307
+ silent: true,
1308
+ sharedInstallLocations: sharedInstallLocations
1309
+ });
1310
+ if (hookSuccess > 0) successfullyInstalled++;
1175
1311
  }
1176
1312
 
1177
1313
  // Handle YAML workflow if provided
@@ -1203,7 +1339,15 @@ async function installMultipleComponents(options, targetDir) {
1203
1339
  }
1204
1340
  }
1205
1341
 
1206
- console.log(chalk.green(`\n✅ Successfully installed ${totalComponents} components!`));
1342
+ if (successfullyInstalled === totalComponents) {
1343
+ console.log(chalk.green(`\n✅ Successfully installed ${successfullyInstalled} components!`));
1344
+ } else if (successfullyInstalled > 0) {
1345
+ console.log(chalk.yellow(`\n⚠️ Successfully installed ${successfullyInstalled} of ${totalComponents} components.`));
1346
+ console.log(chalk.red(`❌ ${totalComponents - successfullyInstalled} component(s) failed to install.`));
1347
+ } else {
1348
+ console.log(chalk.red(`\n❌ No components were installed successfully.`));
1349
+ return; // Exit early if nothing was installed
1350
+ }
1207
1351
  console.log(chalk.cyan(`📁 Components installed to: .claude/`));
1208
1352
 
1209
1353
  if (options.yaml) {
@@ -1211,17 +1355,7 @@ async function installMultipleComponents(options, targetDir) {
1211
1355
  console.log(chalk.cyan(`🚀 Use the workflow file with Claude Code to execute the complete setup`));
1212
1356
  }
1213
1357
 
1214
- // Track installation
1215
- trackingService.trackDownload('multi-component', 'batch', {
1216
- installation_type: 'multi-component',
1217
- agents_count: components.agents.length,
1218
- commands_count: components.commands.length,
1219
- mcps_count: components.mcps.length,
1220
- settings_count: components.settings.length,
1221
- hooks_count: components.hooks.length,
1222
- has_yaml: !!options.yaml,
1223
- target_directory: path.relative(process.cwd(), targetDir)
1224
- });
1358
+ // Note: Individual components are already tracked separately in their installation functions
1225
1359
 
1226
1360
  // Handle prompt execution if provided
1227
1361
  if (options.prompt) {
@@ -1,12 +1,10 @@
1
1
  /**
2
- * TrackingService - Download analytics using GitHub Issues as backend
2
+ * TrackingService - Anonymous download analytics using Supabase database
3
3
  * Records component installations for analytics without impacting user experience
4
4
  */
5
5
 
6
6
  class TrackingService {
7
7
  constructor() {
8
- this.repoOwner = 'davila7';
9
- this.repoName = 'claude-code-templates';
10
8
  this.trackingEnabled = this.shouldEnableTracking();
11
9
  this.timeout = 5000; // 5s timeout for tracking requests
12
10
  }
@@ -83,35 +81,20 @@ class TrackingService {
83
81
  }
84
82
 
85
83
  /**
86
- * Send tracking data via public telemetry endpoint (like Google Analytics)
84
+ * Send tracking data to database endpoint
87
85
  */
88
86
  async sendTrackingData(trackingData) {
89
87
  const controller = new AbortController();
90
88
  const timeoutId = setTimeout(() => controller.abort(), this.timeout);
91
89
 
92
90
  try {
93
- // Build query parameters for GET request (like image tracking)
94
- const params = new URLSearchParams({
95
- type: trackingData.component_type,
96
- name: trackingData.component_name,
97
- platform: trackingData.environment.platform || 'unknown',
98
- cli: trackingData.environment.cli_version || 'unknown',
99
- session: trackingData.session_id.substring(0, 8) // Only first 8 chars for privacy
100
- });
101
-
102
- // Use GitHub Pages tracking endpoint via custom domain (no auth needed)
103
- await fetch(`https://aitmpl.com/api/track.html?${params}`, {
104
- method: 'GET',
105
- mode: 'no-cors', // Prevents CORS errors
106
- signal: controller.signal
107
- });
91
+ // Send to Vercel database endpoint
92
+ await this.sendToDatabase(trackingData, controller.signal);
108
93
 
109
94
  clearTimeout(timeoutId);
110
95
 
111
- // No need to check response with no-cors mode
112
- // Only show success message when debugging
113
96
  if (process.env.CCT_DEBUG === 'true') {
114
- console.debug('📊 Download tracked successfully via telemetry');
97
+ console.debug('📊 Download tracked successfully');
115
98
  }
116
99
 
117
100
  } catch (error) {
@@ -123,6 +106,63 @@ class TrackingService {
123
106
  }
124
107
  }
125
108
 
109
+ /**
110
+ * Send tracking data to Vercel database
111
+ */
112
+ async sendToDatabase(trackingData, signal) {
113
+ try {
114
+ // Extract component path from metadata
115
+ const componentPath = trackingData.metadata?.target_directory ||
116
+ trackingData.metadata?.path ||
117
+ trackingData.component_name;
118
+
119
+ // Extract category from metadata or component name
120
+ const category = trackingData.metadata?.category ||
121
+ (trackingData.component_name.includes('/') ?
122
+ trackingData.component_name.split('/')[0] : 'general');
123
+
124
+ const payload = {
125
+ type: trackingData.component_type,
126
+ name: trackingData.component_name,
127
+ path: componentPath,
128
+ category: category,
129
+ cliVersion: trackingData.environment?.cli_version || 'unknown'
130
+ };
131
+
132
+ const response = await fetch('https://www.aitmpl.com/api/track-download-supabase', {
133
+ method: 'POST',
134
+ headers: {
135
+ 'Content-Type': 'application/json',
136
+ 'User-Agent': `claude-code-templates/${trackingData.environment?.cli_version || 'unknown'}`
137
+ },
138
+ body: JSON.stringify(payload),
139
+ signal: signal
140
+ });
141
+
142
+ if (process.env.CCT_DEBUG === 'true') {
143
+ console.debug('📊 Payload sent:', JSON.stringify(payload, null, 2));
144
+ if (response.ok) {
145
+ console.debug('📊 Successfully saved to database');
146
+ } else {
147
+ console.debug(`📊 Database save failed with status: ${response.status}`);
148
+ try {
149
+ const errorText = await response.text();
150
+ console.debug('📊 Error response:', errorText);
151
+ } catch (e) {
152
+ console.debug('📊 Could not read error response');
153
+ }
154
+ }
155
+ }
156
+
157
+ } catch (error) {
158
+ if (process.env.CCT_DEBUG === 'true') {
159
+ console.debug('📊 Database tracking failed:', error.message);
160
+ }
161
+ // Don't throw - tracking should be non-blocking
162
+ }
163
+ }
164
+
165
+
126
166
  /**
127
167
  * Generate a session ID for grouping related downloads
128
168
  */