claude-code-templates 1.17.1 → 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 +464 -186
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-code-templates",
3
- "version": "1.17.1",
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,115 +586,229 @@ async function installIndividualSetting(settingName, targetDir, options) {
586
586
  delete settingConfig.description;
587
587
  }
588
588
 
589
- // Check if .claude/settings.json exists in target directory
590
- const claudeDir = path.join(targetDir, '.claude');
591
- const targetSettingsFile = path.join(claudeDir, 'settings.json');
592
- let existingConfig = {};
593
-
594
- // Ensure .claude directory exists
595
- await fs.ensureDir(claudeDir);
596
-
597
- if (await fs.pathExists(targetSettingsFile)) {
598
- existingConfig = await fs.readJson(targetSettingsFile);
599
- console.log(chalk.yellow('šŸ“ Existing .claude/settings.json found, merging configurations...'));
600
- }
601
-
602
- // Check for conflicts before merging
603
- const conflicts = [];
604
-
605
- // Check for conflicting environment variables
606
- if (existingConfig.env && settingConfig.env) {
607
- Object.keys(settingConfig.env).forEach(key => {
608
- if (existingConfig.env[key] && existingConfig.env[key] !== settingConfig.env[key]) {
609
- conflicts.push(`Environment variable "${key}" (current: "${existingConfig.env[key]}", new: "${settingConfig.env[key]}")`);
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
+ const inquirer = require('inquirer');
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
+ choices: [
598
+ {
599
+ name: 'šŸ  User settings (~/.claude/settings.json) - Applies to all projects',
600
+ value: 'user'
601
+ },
602
+ {
603
+ name: 'šŸ“ Project settings (.claude/settings.json) - Shared with team',
604
+ value: 'project'
605
+ },
606
+ {
607
+ name: 'āš™ļø Local settings (.claude/settings.local.json) - Personal, not committed',
608
+ value: 'local',
609
+ checked: true // Default selection
610
+ },
611
+ {
612
+ name: 'šŸ¢ Enterprise managed settings - System-wide policy (requires admin)',
613
+ value: 'enterprise'
614
+ }
615
+ ],
616
+ validate: function(answer) {
617
+ if (answer.length < 1) {
618
+ return 'You must choose at least one installation location.';
619
+ }
620
+ return true;
610
621
  }
611
- });
622
+ }]);
623
+
624
+ installLocations = selectedLocations;
612
625
  }
613
626
 
614
- // Check for conflicting top-level settings
615
- Object.keys(settingConfig).forEach(key => {
616
- if (key !== 'permissions' && key !== 'env' && key !== 'hooks' &&
617
- existingConfig[key] !== undefined && existingConfig[key] !== settingConfig[key]) {
618
- conflicts.push(`Setting "${key}" (current: "${existingConfig[key]}", new: "${settingConfig[key]}")`);
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
+
635
+ if (installLocation === 'user') {
636
+ const os = require('os');
637
+ currentTargetDir = os.homedir();
638
+ settingsFile = 'settings.json';
639
+ } else if (installLocation === 'project') {
640
+ settingsFile = 'settings.json';
641
+ } else if (installLocation === 'local') {
642
+ settingsFile = 'settings.local.json';
643
+ } else if (installLocation === 'enterprise') {
644
+ const os = require('os');
645
+ const platform = os.platform();
646
+
647
+ if (platform === 'darwin') {
648
+ // macOS
649
+ currentTargetDir = '/Library/Application Support/ClaudeCode';
650
+ settingsFile = 'managed-settings.json';
651
+ } else if (platform === 'linux' || (process.platform === 'win32' && process.env.WSL_DISTRO_NAME)) {
652
+ // Linux and WSL
653
+ currentTargetDir = '/etc/claude-code';
654
+ settingsFile = 'managed-settings.json';
655
+ } else if (platform === 'win32') {
656
+ // Windows
657
+ currentTargetDir = 'C:\\ProgramData\\ClaudeCode';
658
+ settingsFile = 'managed-settings.json';
659
+ } else {
660
+ console.log(chalk.yellow('āš ļø Platform not supported for enterprise settings. Using user settings instead.'));
661
+ const os = require('os');
662
+ currentTargetDir = os.homedir();
663
+ settingsFile = 'settings.json';
664
+ }
665
+
666
+ console.log(chalk.yellow(`āš ļø Enterprise settings require administrator privileges.`));
667
+ console.log(chalk.gray(`šŸ“ Target path: ${path.join(currentTargetDir, settingsFile)}`));
619
668
  }
620
- });
621
-
622
- // Ask user about conflicts if any exist and not in silent mode
623
- if (conflicts.length > 0 && !options.silent) {
624
- console.log(chalk.yellow(`\nāš ļø Conflicts detected while installing setting "${settingName}":`));
625
- conflicts.forEach(conflict => console.log(chalk.gray(` • ${conflict}`)));
626
669
 
627
- const inquirer = require('inquirer');
628
- const { shouldOverwrite } = await inquirer.prompt([{
629
- type: 'confirm',
630
- name: 'shouldOverwrite',
631
- message: 'Do you want to overwrite the existing configuration?',
632
- default: false
633
- }]);
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 = {};
634
674
 
635
- if (!shouldOverwrite) {
636
- console.log(chalk.yellow(`ā¹ļø Installation of setting "${settingName}" cancelled by user.`));
637
- return;
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
684
+ }
685
+ } else {
686
+ // Ensure .claude directory exists for regular settings
687
+ await fs.ensureDir(claudeDir);
638
688
  }
639
- } else if (conflicts.length > 0 && options.silent) {
640
- // In silent mode (batch installation), skip conflicting settings and warn
641
- console.log(chalk.yellow(`āš ļø Skipping setting "${settingName}" due to conflicts (use individual installation to resolve)`));
642
- return;
643
- }
644
-
645
- // Deep merge configurations
646
- const mergedConfig = {
647
- ...existingConfig,
648
- ...settingConfig
649
- };
650
-
651
- // Deep merge specific sections (only if no conflicts or user approved overwrite)
652
- if (existingConfig.permissions && settingConfig.permissions) {
653
- mergedConfig.permissions = {
654
- ...existingConfig.permissions,
655
- ...settingConfig.permissions
656
- };
657
689
 
658
- // Merge arrays for allow, deny, ask (no conflicts here, just merge)
659
- ['allow', 'deny', 'ask'].forEach(key => {
660
- if (existingConfig.permissions[key] && settingConfig.permissions[key]) {
661
- mergedConfig.permissions[key] = [
662
- ...new Set([...existingConfig.permissions[key], ...settingConfig.permissions[key]])
663
- ];
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
+ }
699
+
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
+ });
710
+ }
711
+
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]}")`);
664
717
  }
665
718
  });
666
- }
667
-
668
- if (existingConfig.env && settingConfig.env) {
669
- mergedConfig.env = {
670
- ...existingConfig.env,
671
- ...settingConfig.env
672
- };
673
- }
674
-
675
- if (existingConfig.hooks && settingConfig.hooks) {
676
- mergedConfig.hooks = {
677
- ...existingConfig.hooks,
678
- ...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
679
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++;
680
799
  }
681
800
 
682
- // Write the merged configuration
683
- await fs.writeJson(targetSettingsFile, mergedConfig, { spaces: 2 });
684
-
801
+ // Summary after all installations
685
802
  if (!options.silent) {
686
- console.log(chalk.green(`āœ… Setting "${settingName}" installed successfully!`));
687
- console.log(chalk.cyan(`šŸ“ Configuration merged into: ${path.relative(targetDir, targetSettingsFile)}`));
688
- 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
+ }
689
810
  }
690
811
 
691
- // Track successful setting installation
692
- trackingService.trackDownload('setting', settingName, {
693
- installation_type: 'individual_setting',
694
- merged_with_existing: Object.keys(existingConfig).length > 0,
695
- source: 'github_main'
696
- });
697
-
698
812
  } catch (error) {
699
813
  console.log(chalk.red(`āŒ Error installing setting: ${error.message}`));
700
814
  }
@@ -734,110 +848,224 @@ async function installIndividualHook(hookName, targetDir, options) {
734
848
  delete hookConfig.description;
735
849
  }
736
850
 
737
- // Check if .claude/settings.json exists in target directory (hooks go in settings.json)
738
- const claudeDir = path.join(targetDir, '.claude');
739
- const targetSettingsFile = path.join(claudeDir, 'settings.json');
740
- let existingConfig = {};
741
-
742
- // Ensure .claude directory exists
743
- await fs.ensureDir(claudeDir);
744
-
745
- if (await fs.pathExists(targetSettingsFile)) {
746
- existingConfig = await fs.readJson(targetSettingsFile);
747
- console.log(chalk.yellow('šŸ“ Existing .claude/settings.json found, merging hook configurations...'));
748
- }
749
-
750
- // Check for conflicts before merging (simplified for new array format)
751
- const conflicts = [];
752
-
753
- // For the new array format, we'll allow appending rather than conflict detection
754
- // This is because Claude Code's array format naturally supports multiple hooks
755
- // Conflicts are less likely and generally hooks can coexist
756
-
757
- // Ask user about conflicts if any exist and not in silent mode
758
- if (conflicts.length > 0 && !options.silent) {
759
- console.log(chalk.yellow(`\nāš ļø Conflicts detected while installing hook "${hookName}":`));
760
- conflicts.forEach(conflict => console.log(chalk.gray(` • ${conflict}`)));
761
-
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) {
762
854
  const inquirer = require('inquirer');
763
- const { shouldOverwrite } = await inquirer.prompt([{
764
- type: 'confirm',
765
- name: 'shouldOverwrite',
766
- message: 'Do you want to overwrite the existing hook configuration?',
767
- default: false
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)',
859
+ choices: [
860
+ {
861
+ name: 'šŸ  User settings (~/.claude/settings.json) - Applies to all projects',
862
+ value: 'user'
863
+ },
864
+ {
865
+ name: 'šŸ“ Project settings (.claude/settings.json) - Shared with team',
866
+ value: 'project'
867
+ },
868
+ {
869
+ name: 'āš™ļø Local settings (.claude/settings.local.json) - Personal, not committed',
870
+ value: 'local',
871
+ checked: true // Default selection
872
+ },
873
+ {
874
+ name: 'šŸ¢ Enterprise managed settings - System-wide policy (requires admin)',
875
+ value: 'enterprise'
876
+ }
877
+ ],
878
+ validate: function(answer) {
879
+ if (answer.length < 1) {
880
+ return 'You must choose at least one installation location.';
881
+ }
882
+ return true;
883
+ }
768
884
  }]);
769
885
 
770
- if (!shouldOverwrite) {
771
- console.log(chalk.yellow(`ā¹ļø Installation of hook "${hookName}" cancelled by user.`));
772
- return;
773
- }
774
- } else if (conflicts.length > 0 && options.silent) {
775
- // In silent mode (batch installation), skip conflicting hooks and warn
776
- console.log(chalk.yellow(`āš ļø Skipping hook "${hookName}" due to conflicts (use individual installation to resolve)`));
777
- return;
778
- }
779
-
780
- // Deep merge configurations with proper hook array structure
781
- const mergedConfig = {
782
- ...existingConfig
783
- };
784
-
785
- // Initialize hooks structure if it doesn't exist
786
- if (!mergedConfig.hooks) {
787
- mergedConfig.hooks = {};
886
+ installLocations = selectedLocations;
788
887
  }
789
888
 
790
- // Merge hook configurations properly (Claude Code expects arrays)
791
- if (hookConfig.hooks) {
792
- Object.keys(hookConfig.hooks).forEach(hookType => {
793
- if (!mergedConfig.hooks[hookType]) {
794
- // If hook type doesn't exist, just copy the array
795
- mergedConfig.hooks[hookType] = hookConfig.hooks[hookType];
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
896
+
897
+ if (installLocation === 'user') {
898
+ const os = require('os');
899
+ currentTargetDir = os.homedir();
900
+ settingsFile = 'settings.json';
901
+ } else if (installLocation === 'project') {
902
+ settingsFile = 'settings.json';
903
+ } else if (installLocation === 'local') {
904
+ settingsFile = 'settings.local.json';
905
+ } else if (installLocation === 'enterprise') {
906
+ const os = require('os');
907
+ const platform = os.platform();
908
+
909
+ if (platform === 'darwin') {
910
+ // macOS
911
+ currentTargetDir = '/Library/Application Support/ClaudeCode';
912
+ settingsFile = 'managed-settings.json';
913
+ } else if (platform === 'linux' || (process.platform === 'win32' && process.env.WSL_DISTRO_NAME)) {
914
+ // Linux and WSL
915
+ currentTargetDir = '/etc/claude-code';
916
+ settingsFile = 'managed-settings.json';
917
+ } else if (platform === 'win32') {
918
+ // Windows
919
+ currentTargetDir = 'C:\\ProgramData\\ClaudeCode';
920
+ settingsFile = 'managed-settings.json';
796
921
  } else {
797
- // If hook type exists, append to the array (Claude Code format)
798
- if (Array.isArray(hookConfig.hooks[hookType])) {
799
- // New format: array of hook objects
800
- if (!Array.isArray(mergedConfig.hooks[hookType])) {
801
- // Convert old format to new format
802
- mergedConfig.hooks[hookType] = [];
803
- }
804
- // Append new hooks to existing array
805
- mergedConfig.hooks[hookType] = mergedConfig.hooks[hookType].concat(hookConfig.hooks[hookType]);
922
+ console.log(chalk.yellow('āš ļø Platform not supported for enterprise settings. Using user settings instead.'));
923
+ const os = require('os');
924
+ currentTargetDir = os.homedir();
925
+ settingsFile = 'settings.json';
926
+ }
927
+
928
+ console.log(chalk.yellow(`āš ļø Enterprise settings require administrator privileges.`));
929
+ console.log(chalk.gray(`šŸ“ Target path: ${path.join(currentTargetDir, settingsFile)}`));
930
+ }
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);
950
+ }
951
+
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
+ }
961
+
962
+ // Check for conflicts before merging (simplified for new array format)
963
+ const conflicts = [];
964
+
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
990
+ }
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];
806
1008
  } else {
807
- // Old format compatibility: convert to new format
808
- console.log(chalk.yellow(`āš ļø Converting old hook format to new Claude Code format for ${hookType}`));
809
- if (!Array.isArray(mergedConfig.hooks[hookType])) {
810
- 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
+ });
811
1032
  }
812
- // Add old format hook as a single matcher
813
- mergedConfig.hooks[hookType].push({
814
- matcher: "*",
815
- hooks: [{
816
- type: "command",
817
- command: hookConfig.hooks[hookType]
818
- }]
819
- });
820
1033
  }
821
- }
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'
822
1052
  });
1053
+
1054
+ // Increment successful installations counter
1055
+ successfulInstallations++;
823
1056
  }
824
1057
 
825
- // Write the merged configuration
826
- await fs.writeJson(targetSettingsFile, mergedConfig, { spaces: 2 });
827
-
1058
+ // Summary after all installations
828
1059
  if (!options.silent) {
829
- console.log(chalk.green(`āœ… Hook "${hookName}" installed successfully!`));
830
- console.log(chalk.cyan(`šŸ“ Configuration merged into: ${path.relative(targetDir, targetSettingsFile)}`));
831
- 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
+ }
832
1067
  }
833
1068
 
834
- // Track successful hook installation
835
- trackingService.trackDownload('hook', hookName, {
836
- installation_type: 'individual_hook',
837
- merged_with_existing: Object.keys(existingConfig).length > 0,
838
- source: 'github_main'
839
- });
840
-
841
1069
  } catch (error) {
842
1070
  console.log(chalk.red(`āŒ Error installing hook: ${error.message}`));
843
1071
  }
@@ -980,6 +1208,48 @@ async function installMultipleComponents(options, targetDir) {
980
1208
  console.log(chalk.gray(` Settings: ${components.settings.length}`));
981
1209
  console.log(chalk.gray(` Hooks: ${components.hooks.length}`));
982
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
+
983
1253
  // Install agents
984
1254
  for (const agent of components.agents) {
985
1255
  console.log(chalk.gray(` Installing agent: ${agent}`));
@@ -998,16 +1268,24 @@ async function installMultipleComponents(options, targetDir) {
998
1268
  await installIndividualMCP(mcp, targetDir, { ...options, silent: true });
999
1269
  }
1000
1270
 
1001
- // Install settings
1271
+ // Install settings (using shared installation locations)
1002
1272
  for (const setting of components.settings) {
1003
1273
  console.log(chalk.gray(` Installing setting: ${setting}`));
1004
- await installIndividualSetting(setting, targetDir, { ...options, silent: true });
1274
+ await installIndividualSetting(setting, targetDir, {
1275
+ ...options,
1276
+ silent: true,
1277
+ sharedInstallLocations: sharedInstallLocations
1278
+ });
1005
1279
  }
1006
1280
 
1007
- // Install hooks
1281
+ // Install hooks (using shared installation locations)
1008
1282
  for (const hook of components.hooks) {
1009
1283
  console.log(chalk.gray(` Installing hook: ${hook}`));
1010
- await installIndividualHook(hook, targetDir, { ...options, silent: true });
1284
+ await installIndividualHook(hook, targetDir, {
1285
+ ...options,
1286
+ silent: true,
1287
+ sharedInstallLocations: sharedInstallLocations
1288
+ });
1011
1289
  }
1012
1290
 
1013
1291
  // Handle YAML workflow if provided