claude-code-templates 1.18.0 → 1.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/index.js +371 -257
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.0",
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": {
package/src/index.js CHANGED
@@ -586,14 +586,14 @@ async function installIndividualSetting(settingName, targetDir, options) {
586
586
  delete settingConfig.description;
587
587
  }
588
588
 
589
- // Ask user where to install the setting (unless in silent mode)
590
- let settingsFile = 'settings.json'; // default
591
- if (!options.silent) {
589
+ // Use shared locations if provided (batch mode), otherwise ask user
590
+ let installLocations = options.sharedInstallLocations || ['local']; // default to local settings
591
+ if (!options.silent && !options.sharedInstallLocations) {
592
592
  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?',
593
+ const { selectedLocations } = await inquirer.prompt([{
594
+ type: 'checkbox',
595
+ name: 'selectedLocations',
596
+ message: 'Where would you like to install this setting? (Select one or more)',
597
597
  choices: [
598
598
  {
599
599
  name: 'šŸ  User settings (~/.claude/settings.json) - Applies to all projects',
@@ -605,19 +605,36 @@ async function installIndividualSetting(settingName, targetDir, options) {
605
605
  },
606
606
  {
607
607
  name: 'āš™ļø Local settings (.claude/settings.local.json) - Personal, not committed',
608
- value: 'local'
608
+ value: 'local',
609
+ checked: true // Default selection
609
610
  },
610
611
  {
611
612
  name: 'šŸ¢ Enterprise managed settings - System-wide policy (requires admin)',
612
613
  value: 'enterprise'
613
614
  }
614
615
  ],
615
- default: 'local'
616
+ validate: function(answer) {
617
+ if (answer.length < 1) {
618
+ return 'You must choose at least one installation location.';
619
+ }
620
+ return true;
621
+ }
616
622
  }]);
617
-
623
+
624
+ installLocations = selectedLocations;
625
+ }
626
+
627
+ // Install the setting in each selected location
628
+ let successfulInstallations = 0;
629
+ for (const installLocation of installLocations) {
630
+ console.log(chalk.blue(`\nšŸ“ Installing "${settingName}" in ${installLocation} settings...`));
631
+
632
+ let currentTargetDir = targetDir;
633
+ let settingsFile = 'settings.local.json'; // default
634
+
618
635
  if (installLocation === 'user') {
619
636
  const os = require('os');
620
- targetDir = os.homedir();
637
+ currentTargetDir = os.homedir();
621
638
  settingsFile = 'settings.json';
622
639
  } else if (installLocation === 'project') {
623
640
  settingsFile = 'settings.json';
@@ -629,154 +646,169 @@ async function installIndividualSetting(settingName, targetDir, options) {
629
646
 
630
647
  if (platform === 'darwin') {
631
648
  // macOS
632
- targetDir = '/Library/Application Support/ClaudeCode';
649
+ currentTargetDir = '/Library/Application Support/ClaudeCode';
633
650
  settingsFile = 'managed-settings.json';
634
651
  } else if (platform === 'linux' || (process.platform === 'win32' && process.env.WSL_DISTRO_NAME)) {
635
652
  // Linux and WSL
636
- targetDir = '/etc/claude-code';
653
+ currentTargetDir = '/etc/claude-code';
637
654
  settingsFile = 'managed-settings.json';
638
655
  } else if (platform === 'win32') {
639
656
  // Windows
640
- targetDir = 'C:\\ProgramData\\ClaudeCode';
657
+ currentTargetDir = 'C:\\ProgramData\\ClaudeCode';
641
658
  settingsFile = 'managed-settings.json';
642
659
  } else {
643
660
  console.log(chalk.yellow('āš ļø Platform not supported for enterprise settings. Using user settings instead.'));
644
661
  const os = require('os');
645
- targetDir = os.homedir();
662
+ currentTargetDir = os.homedir();
646
663
  settingsFile = 'settings.json';
647
664
  }
648
665
 
649
666
  console.log(chalk.yellow(`āš ļø Enterprise settings require administrator privileges.`));
650
- console.log(chalk.gray(`šŸ“ Target path: ${path.join(targetDir, settingsFile)}`));
651
- }
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;
667
+ console.log(chalk.gray(`šŸ“ Target path: ${path.join(currentTargetDir, settingsFile)}`));
668
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
669
 
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]}")`);
670
+ // Determine target directory and file based on selection
671
+ const claudeDir = path.join(currentTargetDir, '.claude');
672
+ const targetSettingsFile = path.join(claudeDir, settingsFile);
673
+ let existingConfig = {};
674
+
675
+ // For enterprise settings, create directory structure directly (not under .claude)
676
+ if (settingsFile === 'managed-settings.json') {
677
+ // Ensure enterprise directory exists (requires admin privileges)
678
+ try {
679
+ await fs.ensureDir(currentTargetDir);
680
+ } catch (error) {
681
+ console.log(chalk.red(`āŒ Failed to create enterprise directory: ${error.message}`));
682
+ console.log(chalk.yellow('šŸ’” Try running with administrator privileges or choose a different installation location.'));
683
+ continue; // Skip this location and continue with others
692
684
  }
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]}")`);
685
+ } else {
686
+ // Ensure .claude directory exists for regular settings
687
+ await fs.ensureDir(claudeDir);
701
688
  }
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
689
 
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
- }]);
690
+ // Read existing configuration
691
+ const actualTargetFile = settingsFile === 'managed-settings.json'
692
+ ? path.join(currentTargetDir, settingsFile)
693
+ : targetSettingsFile;
694
+
695
+ if (await fs.pathExists(actualTargetFile)) {
696
+ existingConfig = await fs.readJson(actualTargetFile);
697
+ console.log(chalk.yellow(`šŸ“ Existing ${settingsFile} found, merging configurations...`));
698
+ }
716
699
 
717
- if (!shouldOverwrite) {
718
- console.log(chalk.yellow(`ā¹ļø Installation of setting "${settingName}" cancelled by user.`));
719
- return;
700
+ // Check for conflicts before merging
701
+ const conflicts = [];
702
+
703
+ // Check for conflicting environment variables
704
+ if (existingConfig.env && settingConfig.env) {
705
+ Object.keys(settingConfig.env).forEach(key => {
706
+ if (existingConfig.env[key] && existingConfig.env[key] !== settingConfig.env[key]) {
707
+ conflicts.push(`Environment variable "${key}" (current: "${existingConfig.env[key]}", new: "${settingConfig.env[key]}")`);
708
+ }
709
+ });
720
710
  }
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
711
 
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
- ];
712
+ // Check for conflicting top-level settings
713
+ Object.keys(settingConfig).forEach(key => {
714
+ if (key !== 'permissions' && key !== 'env' && key !== 'hooks' &&
715
+ existingConfig[key] !== undefined && existingConfig[key] !== settingConfig[key]) {
716
+ conflicts.push(`Setting "${key}" (current: "${existingConfig[key]}", new: "${settingConfig[key]}")`);
746
717
  }
747
718
  });
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
719
+
720
+ // Ask user about conflicts if any exist and not in silent mode
721
+ if (conflicts.length > 0 && !options.silent) {
722
+ console.log(chalk.yellow(`\nāš ļø Conflicts detected while installing setting "${settingName}" in ${installLocation}:`));
723
+ conflicts.forEach(conflict => console.log(chalk.gray(` • ${conflict}`)));
724
+
725
+ const inquirer = require('inquirer');
726
+ const { shouldOverwrite } = await inquirer.prompt([{
727
+ type: 'confirm',
728
+ name: 'shouldOverwrite',
729
+ message: `Do you want to overwrite the existing configuration in ${installLocation}?`,
730
+ default: false
731
+ }]);
732
+
733
+ if (!shouldOverwrite) {
734
+ console.log(chalk.yellow(`ā¹ļø Installation of setting "${settingName}" in ${installLocation} cancelled by user.`));
735
+ continue; // Skip this location and continue with others
736
+ }
737
+ } else if (conflicts.length > 0 && options.silent) {
738
+ // In silent mode (batch installation), skip conflicting settings and warn
739
+ console.log(chalk.yellow(`āš ļø Skipping setting "${settingName}" in ${installLocation} due to conflicts (use individual installation to resolve)`));
740
+ continue; // Skip this location and continue with others
741
+ }
742
+
743
+ // Deep merge configurations
744
+ const mergedConfig = {
745
+ ...existingConfig,
746
+ ...settingConfig
761
747
  };
748
+
749
+ // Deep merge specific sections (only if no conflicts or user approved overwrite)
750
+ if (existingConfig.permissions && settingConfig.permissions) {
751
+ mergedConfig.permissions = {
752
+ ...existingConfig.permissions,
753
+ ...settingConfig.permissions
754
+ };
755
+
756
+ // Merge arrays for allow, deny, ask (no conflicts here, just merge)
757
+ ['allow', 'deny', 'ask'].forEach(key => {
758
+ if (existingConfig.permissions[key] && settingConfig.permissions[key]) {
759
+ mergedConfig.permissions[key] = [
760
+ ...new Set([...existingConfig.permissions[key], ...settingConfig.permissions[key]])
761
+ ];
762
+ }
763
+ });
764
+ }
765
+
766
+ if (existingConfig.env && settingConfig.env) {
767
+ mergedConfig.env = {
768
+ ...existingConfig.env,
769
+ ...settingConfig.env
770
+ };
771
+ }
772
+
773
+ if (existingConfig.hooks && settingConfig.hooks) {
774
+ mergedConfig.hooks = {
775
+ ...existingConfig.hooks,
776
+ ...settingConfig.hooks
777
+ };
778
+ }
779
+
780
+ // Write the merged configuration
781
+ await fs.writeJson(actualTargetFile, mergedConfig, { spaces: 2 });
782
+
783
+ if (!options.silent) {
784
+ console.log(chalk.green(`āœ… Setting "${settingName}" installed successfully in ${installLocation}!`));
785
+ console.log(chalk.cyan(`šŸ“ Configuration merged into: ${actualTargetFile}`));
786
+ console.log(chalk.cyan(`šŸ“¦ Downloaded from: ${githubUrl}`));
787
+ }
788
+
789
+ // Track successful setting installation for this location
790
+ trackingService.trackDownload('setting', settingName, {
791
+ installation_type: 'individual_setting',
792
+ installation_location: installLocation,
793
+ merged_with_existing: Object.keys(existingConfig).length > 0,
794
+ source: 'github_main'
795
+ });
796
+
797
+ // Increment successful installations counter
798
+ successfulInstallations++;
762
799
  }
763
800
 
764
- // Write the merged configuration
765
- await fs.writeJson(actualTargetFile, mergedConfig, { spaces: 2 });
766
-
801
+ // Summary after all installations
767
802
  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}`));
803
+ if (successfulInstallations === installLocations.length) {
804
+ console.log(chalk.green(`\nšŸŽ‰ Setting "${settingName}" successfully installed in ${successfulInstallations} location(s)!`));
805
+ } else {
806
+ console.log(chalk.yellow(`\nāš ļø Setting "${settingName}" installed in ${successfulInstallations} of ${installLocations.length} location(s).`));
807
+ const failedCount = installLocations.length - successfulInstallations;
808
+ console.log(chalk.red(`āŒ ${failedCount} installation(s) failed due to permission or other errors.`));
809
+ }
771
810
  }
772
811
 
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
- });
779
-
780
812
  } catch (error) {
781
813
  console.log(chalk.red(`āŒ Error installing setting: ${error.message}`));
782
814
  }
@@ -816,14 +848,14 @@ async function installIndividualHook(hookName, targetDir, options) {
816
848
  delete hookConfig.description;
817
849
  }
818
850
 
819
- // Ask user where to install the hook (unless in silent mode)
820
- let settingsFile = 'settings.json'; // default
821
- if (!options.silent) {
851
+ // Use shared locations if provided (batch mode), otherwise ask user
852
+ let installLocations = options.sharedInstallLocations || ['local']; // default to local settings
853
+ if (!options.silent && !options.sharedInstallLocations) {
822
854
  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?',
855
+ const { selectedLocations } = await inquirer.prompt([{
856
+ type: 'checkbox',
857
+ name: 'selectedLocations',
858
+ message: 'Where would you like to install this hook? (Select one or more)',
827
859
  choices: [
828
860
  {
829
861
  name: 'šŸ  User settings (~/.claude/settings.json) - Applies to all projects',
@@ -835,19 +867,36 @@ async function installIndividualHook(hookName, targetDir, options) {
835
867
  },
836
868
  {
837
869
  name: 'āš™ļø Local settings (.claude/settings.local.json) - Personal, not committed',
838
- value: 'local'
870
+ value: 'local',
871
+ checked: true // Default selection
839
872
  },
840
873
  {
841
874
  name: 'šŸ¢ Enterprise managed settings - System-wide policy (requires admin)',
842
875
  value: 'enterprise'
843
876
  }
844
877
  ],
845
- default: 'local'
878
+ validate: function(answer) {
879
+ if (answer.length < 1) {
880
+ return 'You must choose at least one installation location.';
881
+ }
882
+ return true;
883
+ }
846
884
  }]);
885
+
886
+ installLocations = selectedLocations;
887
+ }
888
+
889
+ // Install the hook in each selected location
890
+ let successfulInstallations = 0;
891
+ for (const installLocation of installLocations) {
892
+ console.log(chalk.blue(`\nšŸ“ Installing "${hookName}" in ${installLocation} settings...`));
893
+
894
+ let currentTargetDir = targetDir;
895
+ let settingsFile = 'settings.local.json'; // default
847
896
 
848
897
  if (installLocation === 'user') {
849
898
  const os = require('os');
850
- targetDir = os.homedir();
899
+ currentTargetDir = os.homedir();
851
900
  settingsFile = 'settings.json';
852
901
  } else if (installLocation === 'project') {
853
902
  settingsFile = 'settings.json';
@@ -859,149 +908,164 @@ async function installIndividualHook(hookName, targetDir, options) {
859
908
 
860
909
  if (platform === 'darwin') {
861
910
  // macOS
862
- targetDir = '/Library/Application Support/ClaudeCode';
911
+ currentTargetDir = '/Library/Application Support/ClaudeCode';
863
912
  settingsFile = 'managed-settings.json';
864
913
  } else if (platform === 'linux' || (process.platform === 'win32' && process.env.WSL_DISTRO_NAME)) {
865
914
  // Linux and WSL
866
- targetDir = '/etc/claude-code';
915
+ currentTargetDir = '/etc/claude-code';
867
916
  settingsFile = 'managed-settings.json';
868
917
  } else if (platform === 'win32') {
869
918
  // Windows
870
- targetDir = 'C:\\ProgramData\\ClaudeCode';
919
+ currentTargetDir = 'C:\\ProgramData\\ClaudeCode';
871
920
  settingsFile = 'managed-settings.json';
872
921
  } else {
873
922
  console.log(chalk.yellow('āš ļø Platform not supported for enterprise settings. Using user settings instead.'));
874
923
  const os = require('os');
875
- targetDir = os.homedir();
924
+ currentTargetDir = os.homedir();
876
925
  settingsFile = 'settings.json';
877
926
  }
878
927
 
879
928
  console.log(chalk.yellow(`āš ļø Enterprise settings require administrator privileges.`));
880
- console.log(chalk.gray(`šŸ“ Target path: ${path.join(targetDir, settingsFile)}`));
929
+ console.log(chalk.gray(`šŸ“ Target path: ${path.join(currentTargetDir, settingsFile)}`));
881
930
  }
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;
931
+
932
+ // Determine target directory and file based on selection
933
+ const claudeDir = path.join(currentTargetDir, '.claude');
934
+ const targetSettingsFile = path.join(claudeDir, settingsFile);
935
+ let existingConfig = {};
936
+
937
+ // For enterprise settings, create directory structure directly (not under .claude)
938
+ if (settingsFile === 'managed-settings.json') {
939
+ // Ensure enterprise directory exists (requires admin privileges)
940
+ try {
941
+ await fs.ensureDir(currentTargetDir);
942
+ } catch (error) {
943
+ console.log(chalk.red(`āŒ Failed to create enterprise directory: ${error.message}`));
944
+ console.log(chalk.yellow('šŸ’” Try running with administrator privileges or choose a different installation location.'));
945
+ continue; // Skip this location and continue with others
946
+ }
947
+ } else {
948
+ // Ensure .claude directory exists for regular settings
949
+ await fs.ensureDir(claudeDir);
898
950
  }
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
951
 
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}`)));
952
+ // Read existing configuration
953
+ const actualTargetFile = settingsFile === 'managed-settings.json'
954
+ ? path.join(currentTargetDir, settingsFile)
955
+ : targetSettingsFile;
956
+
957
+ if (await fs.pathExists(actualTargetFile)) {
958
+ existingConfig = await fs.readJson(actualTargetFile);
959
+ console.log(chalk.yellow(`šŸ“ Existing ${settingsFile} found, merging hook configurations...`));
960
+ }
925
961
 
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
- }]);
962
+ // Check for conflicts before merging (simplified for new array format)
963
+ const conflicts = [];
933
964
 
934
- if (!shouldOverwrite) {
935
- console.log(chalk.yellow(`ā¹ļø Installation of hook "${hookName}" cancelled by user.`));
936
- return;
965
+ // For the new array format, we'll allow appending rather than conflict detection
966
+ // This is because Claude Code's array format naturally supports multiple hooks
967
+ // Conflicts are less likely and generally hooks can coexist
968
+
969
+ // Ask user about conflicts if any exist and not in silent mode
970
+ if (conflicts.length > 0 && !options.silent) {
971
+ console.log(chalk.yellow(`\nāš ļø Conflicts detected while installing hook "${hookName}" in ${installLocation}:`));
972
+ conflicts.forEach(conflict => console.log(chalk.gray(` • ${conflict}`)));
973
+
974
+ const inquirer = require('inquirer');
975
+ const { shouldOverwrite } = await inquirer.prompt([{
976
+ type: 'confirm',
977
+ name: 'shouldOverwrite',
978
+ message: `Do you want to overwrite the existing hook configuration in ${installLocation}?`,
979
+ default: false
980
+ }]);
981
+
982
+ if (!shouldOverwrite) {
983
+ console.log(chalk.yellow(`ā¹ļø Installation of hook "${hookName}" in ${installLocation} cancelled by user.`));
984
+ continue; // Skip this location and continue with others
985
+ }
986
+ } else if (conflicts.length > 0 && options.silent) {
987
+ // In silent mode (batch installation), skip conflicting hooks and warn
988
+ console.log(chalk.yellow(`āš ļø Skipping hook "${hookName}" in ${installLocation} due to conflicts (use individual installation to resolve)`));
989
+ continue; // Skip this location and continue with others
937
990
  }
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]);
991
+
992
+ // Deep merge configurations with proper hook array structure
993
+ const mergedConfig = {
994
+ ...existingConfig
995
+ };
996
+
997
+ // Initialize hooks structure if it doesn't exist
998
+ if (!mergedConfig.hooks) {
999
+ mergedConfig.hooks = {};
1000
+ }
1001
+
1002
+ // Merge hook configurations properly (Claude Code expects arrays)
1003
+ if (hookConfig.hooks) {
1004
+ Object.keys(hookConfig.hooks).forEach(hookType => {
1005
+ if (!mergedConfig.hooks[hookType]) {
1006
+ // If hook type doesn't exist, just copy the array
1007
+ mergedConfig.hooks[hookType] = hookConfig.hooks[hookType];
970
1008
  } 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] = [];
1009
+ // If hook type exists, append to the array (Claude Code format)
1010
+ if (Array.isArray(hookConfig.hooks[hookType])) {
1011
+ // New format: array of hook objects
1012
+ if (!Array.isArray(mergedConfig.hooks[hookType])) {
1013
+ // Convert old format to new format
1014
+ mergedConfig.hooks[hookType] = [];
1015
+ }
1016
+ // Append new hooks to existing array
1017
+ mergedConfig.hooks[hookType] = mergedConfig.hooks[hookType].concat(hookConfig.hooks[hookType]);
1018
+ } else {
1019
+ // Old format compatibility: convert to new format
1020
+ console.log(chalk.yellow(`āš ļø Converting old hook format to new Claude Code format for ${hookType}`));
1021
+ if (!Array.isArray(mergedConfig.hooks[hookType])) {
1022
+ mergedConfig.hooks[hookType] = [];
1023
+ }
1024
+ // Add old format hook as a single matcher
1025
+ mergedConfig.hooks[hookType].push({
1026
+ matcher: "*",
1027
+ hooks: [{
1028
+ type: "command",
1029
+ command: hookConfig.hooks[hookType]
1030
+ }]
1031
+ });
975
1032
  }
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
1033
  }
985
- }
1034
+ });
1035
+ }
1036
+
1037
+ // Write the merged configuration
1038
+ await fs.writeJson(actualTargetFile, mergedConfig, { spaces: 2 });
1039
+
1040
+ if (!options.silent) {
1041
+ console.log(chalk.green(`āœ… Hook "${hookName}" installed successfully in ${installLocation}!`));
1042
+ console.log(chalk.cyan(`šŸ“ Configuration merged into: ${actualTargetFile}`));
1043
+ console.log(chalk.cyan(`šŸ“¦ Downloaded from: ${githubUrl}`));
1044
+ }
1045
+
1046
+ // Track successful hook installation for this location
1047
+ trackingService.trackDownload('hook', hookName, {
1048
+ installation_type: 'individual_hook',
1049
+ installation_location: installLocation,
1050
+ merged_with_existing: Object.keys(existingConfig).length > 0,
1051
+ source: 'github_main'
986
1052
  });
1053
+
1054
+ // Increment successful installations counter
1055
+ successfulInstallations++;
987
1056
  }
988
1057
 
989
- // Write the merged configuration
990
- await fs.writeJson(actualTargetFile, mergedConfig, { spaces: 2 });
991
-
1058
+ // Summary after all installations
992
1059
  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}`));
1060
+ if (successfulInstallations === installLocations.length) {
1061
+ console.log(chalk.green(`\nšŸŽ‰ Hook "${hookName}" successfully installed in ${successfulInstallations} location(s)!`));
1062
+ } else {
1063
+ console.log(chalk.yellow(`\nāš ļø Hook "${hookName}" installed in ${successfulInstallations} of ${installLocations.length} location(s).`));
1064
+ const failedCount = installLocations.length - successfulInstallations;
1065
+ console.log(chalk.red(`āŒ ${failedCount} installation(s) failed due to permission or other errors.`));
1066
+ }
996
1067
  }
997
1068
 
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
- });
1004
-
1005
1069
  } catch (error) {
1006
1070
  console.log(chalk.red(`āŒ Error installing hook: ${error.message}`));
1007
1071
  }
@@ -1144,6 +1208,48 @@ async function installMultipleComponents(options, targetDir) {
1144
1208
  console.log(chalk.gray(` Settings: ${components.settings.length}`));
1145
1209
  console.log(chalk.gray(` Hooks: ${components.hooks.length}`));
1146
1210
 
1211
+ // Ask for installation locations once for configuration components (if any exist and not in silent mode)
1212
+ let sharedInstallLocations = ['local']; // default
1213
+ const hasSettingsOrHooks = components.settings.length > 0 || components.hooks.length > 0;
1214
+
1215
+ if (hasSettingsOrHooks && !options.yes) {
1216
+ console.log(chalk.blue('\nšŸ“ Choose installation locations for configuration components:'));
1217
+ const inquirer = require('inquirer');
1218
+ const { selectedLocations } = await inquirer.prompt([{
1219
+ type: 'checkbox',
1220
+ name: 'selectedLocations',
1221
+ message: 'Where would you like to install the configuration components? (Select one or more)',
1222
+ choices: [
1223
+ {
1224
+ name: 'šŸ  User settings (~/.claude/settings.json) - Applies to all projects',
1225
+ value: 'user'
1226
+ },
1227
+ {
1228
+ name: 'šŸ“ Project settings (.claude/settings.json) - Shared with team',
1229
+ value: 'project'
1230
+ },
1231
+ {
1232
+ name: 'āš™ļø Local settings (.claude/settings.local.json) - Personal, not committed',
1233
+ value: 'local',
1234
+ checked: true // Default selection
1235
+ },
1236
+ {
1237
+ name: 'šŸ¢ Enterprise managed settings - System-wide policy (requires admin)',
1238
+ value: 'enterprise'
1239
+ }
1240
+ ],
1241
+ validate: function(answer) {
1242
+ if (answer.length < 1) {
1243
+ return 'You must choose at least one installation location.';
1244
+ }
1245
+ return true;
1246
+ }
1247
+ }]);
1248
+
1249
+ sharedInstallLocations = selectedLocations;
1250
+ console.log(chalk.cyan(`šŸ“‹ Will install configuration components in: ${sharedInstallLocations.join(', ')}`));
1251
+ }
1252
+
1147
1253
  // Install agents
1148
1254
  for (const agent of components.agents) {
1149
1255
  console.log(chalk.gray(` Installing agent: ${agent}`));
@@ -1162,16 +1268,24 @@ async function installMultipleComponents(options, targetDir) {
1162
1268
  await installIndividualMCP(mcp, targetDir, { ...options, silent: true });
1163
1269
  }
1164
1270
 
1165
- // Install settings
1271
+ // Install settings (using shared installation locations)
1166
1272
  for (const setting of components.settings) {
1167
1273
  console.log(chalk.gray(` Installing setting: ${setting}`));
1168
- await installIndividualSetting(setting, targetDir, { ...options, silent: true });
1274
+ await installIndividualSetting(setting, targetDir, {
1275
+ ...options,
1276
+ silent: true,
1277
+ sharedInstallLocations: sharedInstallLocations
1278
+ });
1169
1279
  }
1170
1280
 
1171
- // Install hooks
1281
+ // Install hooks (using shared installation locations)
1172
1282
  for (const hook of components.hooks) {
1173
1283
  console.log(chalk.gray(` Installing hook: ${hook}`));
1174
- await installIndividualHook(hook, targetDir, { ...options, silent: true });
1284
+ await installIndividualHook(hook, targetDir, {
1285
+ ...options,
1286
+ silent: true,
1287
+ sharedInstallLocations: sharedInstallLocations
1288
+ });
1175
1289
  }
1176
1290
 
1177
1291
  // Handle YAML workflow if provided