@@ -137,8 +137,8 @@ def debug_log(message: str) -> None:
137137
138138
139139# Default number of parallel workers - can be overridden via CLAUDE_PARALLEL_WORKERS env var
140- # Reduced from 5 to 3 to decrease likelihood of hitting GitHub secondary rate limits
141- DEFAULT_PARALLEL_WORKERS = int (os .environ .get ('CLAUDE_PARALLEL_WORKERS' , '3 ' ))
140+ # Reduced from 5 to 2 to decrease likelihood of hitting GitHub secondary rate limits
141+ DEFAULT_PARALLEL_WORKERS = int (os .environ .get ('CLAUDE_PARALLEL_WORKERS' , '2 ' ))
142142
143143
144144def is_parallel_mode_enabled () -> bool :
@@ -155,6 +155,7 @@ def execute_parallel(
155155 items : list [T ],
156156 func : Callable [[T ], R ],
157157 max_workers : int = DEFAULT_PARALLEL_WORKERS ,
158+ stagger_delay : float = 0.0 ,
158159) -> list [R ]:
159160 """Execute a function on items in parallel with error isolation.
160161
@@ -164,7 +165,9 @@ def execute_parallel(
164165 Args:
165166 items: List of items to process
166167 func: Function to apply to each item
167- max_workers: Maximum number of parallel workers (default: 5)
168+ max_workers: Maximum number of parallel workers (default: 2)
169+ stagger_delay: Delay in seconds between task submissions to prevent
170+ thundering herd on rate-limited APIs (default: 0.0)
168171
169172 Returns:
170173 List of results in the same order as input items.
@@ -186,10 +189,12 @@ def execute_parallel(
186189 results_with_index : list [tuple [int , R | BaseException ]] = []
187190
188191 with concurrent .futures .ThreadPoolExecutor (max_workers = max_workers ) as executor :
189- # Submit all tasks with their index for ordering
190- future_to_index : dict [concurrent .futures .Future [R ], int ] = {
191- executor .submit (func , item ): idx for idx , item in enumerate (items )
192- }
192+ # Submit tasks with optional stagger delay to prevent thundering herd
193+ future_to_index : dict [concurrent .futures .Future [R ], int ] = {}
194+ for idx , item in enumerate (items ):
195+ future_to_index [executor .submit (func , item )] = idx
196+ if stagger_delay > 0 and idx < len (items ) - 1 :
197+ time .sleep (stagger_delay )
193198
194199 # Collect results as they complete
195200 for future in concurrent .futures .as_completed (future_to_index ):
@@ -228,6 +233,7 @@ def execute_parallel_safe(
228233 func : Callable [[T ], R ],
229234 default_on_error : R ,
230235 max_workers : int = DEFAULT_PARALLEL_WORKERS ,
236+ stagger_delay : float = 0.0 ,
231237) -> list [R ]:
232238 """Execute a function on items in parallel with error handling.
233239
@@ -238,7 +244,9 @@ def execute_parallel_safe(
238244 items: List of items to process
239245 func: Function to apply to each item
240246 default_on_error: Value to return for items that raise exceptions
241- max_workers: Maximum number of parallel workers (default: 5)
247+ max_workers: Maximum number of parallel workers (default: 2)
248+ stagger_delay: Delay in seconds between task submissions to prevent
249+ thundering herd on rate-limited APIs (default: 0.0)
242250
243251 Returns:
244252 List of results in the same order as input items.
@@ -254,7 +262,7 @@ def safe_func(item: T) -> R:
254262 debug_log (f'Item processing failed: { exc } ' )
255263 return default_on_error
256264
257- return execute_parallel (items , safe_func , max_workers )
265+ return execute_parallel (items , safe_func , max_workers , stagger_delay = stagger_delay )
258266
259267
260268# Windows UAC elevation helper functions
@@ -3489,8 +3497,8 @@ def execute_dependency(dep: str) -> bool:
34893497def fetch_with_retry [T ](
34903498 request_func : Callable [[], T ],
34913499 url : str ,
3492- max_retries : int = 3 ,
3493- initial_backoff : float = 1 .0 ,
3500+ max_retries : int = 10 ,
3501+ initial_backoff : float = 2 .0 ,
34943502) -> T :
34953503 """Execute a fetch operation with retry logic for rate limiting.
34963504
@@ -3500,8 +3508,8 @@ def fetch_with_retry[T](
35003508 Args:
35013509 request_func: Function that performs the actual request and returns result
35023510 url: URL being fetched (for logging purposes)
3503- max_retries: Maximum number of retry attempts (default: 3 )
3504- initial_backoff: Initial backoff time in seconds (default: 1 .0)
3511+ max_retries: Maximum number of retry attempts (default: 10 )
3512+ initial_backoff: Initial backoff time in seconds (default: 2 .0)
35053513
35063514 Returns:
35073515 Result from request_func
@@ -3889,8 +3897,8 @@ def download_single_resource(task: tuple[str, Path]) -> bool:
38893897 resource , destination = task
38903898 return handle_resource (resource , destination , config_source , base_url , auth_param )
38913899
3892- # Execute downloads in parallel (or sequential if CLAUDE_SEQUENTIAL_MODE=1)
3893- results = execute_parallel_safe (download_tasks , download_single_resource , False )
3900+ # Execute downloads in parallel with stagger delay to avoid rate limiting
3901+ results = execute_parallel_safe (download_tasks , download_single_resource , False , stagger_delay = 0.5 )
38943902 return all (results )
38953903
38963904
@@ -3949,7 +3957,7 @@ def process_file_downloads(
39493957 continue
39503958
39513959 # Expand destination path (~ and environment variables)
3952- # This makes it work cross-platform: ~/.config, %USERPROFILE%\bin, $HOME/.local
3960+ # This makes it work cross-platform: ~/.config, %USERPROFILE%\\ bin, $HOME/.local
39533961 expanded_dest = os .path .expanduser (str (dest ))
39543962 expanded_dest = os .path .expandvars (expanded_dest )
39553963 dest_path = Path (expanded_dest )
@@ -3970,9 +3978,9 @@ def download_single_file(download_info: tuple[str, Path]) -> bool:
39703978 source , dest_path = download_info
39713979 return handle_resource (source , dest_path , config_source , base_url , auth_param )
39723980
3973- # Execute downloads in parallel (or sequential if CLAUDE_SEQUENTIAL_MODE=1)
3981+ # Execute downloads in parallel with stagger delay to avoid rate limiting
39743982 if valid_downloads :
3975- download_results = execute_parallel_safe (valid_downloads , download_single_file , False )
3983+ download_results = execute_parallel_safe (valid_downloads , download_single_file , False , stagger_delay = 0.5 )
39763984 success_count = sum (1 for result in download_results if result )
39773985 failed_count = len (download_results ) - success_count + invalid_count
39783986 else :
@@ -4115,8 +4123,8 @@ def install_single_skill(skill_config: dict[str, Any]) -> bool:
41154123 """Install a single skill and return success status."""
41164124 return process_skill (skill_config , skills_dir , config_source , auth_param )
41174125
4118- # Execute skill installations in parallel (or sequential if CLAUDE_SEQUENTIAL_MODE=1)
4119- results = execute_parallel_safe (skills_config , install_single_skill , False )
4126+ # Execute skill installations in parallel with stagger delay to avoid rate limiting
4127+ results = execute_parallel_safe (skills_config , install_single_skill , False , stagger_delay = 0.5 )
41204128 return all (results )
41214129
41224130
@@ -4892,8 +4900,8 @@ def download_single_hook(task: tuple[str, Path]) -> bool:
48924900 file , destination = task
48934901 return handle_resource (file , destination , config_source , base_url , auth_param )
48944902
4895- # Execute downloads in parallel (or sequential if CLAUDE_SEQUENTIAL_MODE=1)
4896- results = execute_parallel_safe (download_tasks , download_single_hook , False )
4903+ # Execute downloads in parallel with stagger delay to avoid rate limiting
4904+ results = execute_parallel_safe (download_tasks , download_single_hook , False , stagger_delay = 0.5 )
48974905 return all (results )
48984906
48994907
@@ -6184,12 +6192,16 @@ def main() -> None:
61846192 # Ensure .local/bin is in PATH early to prevent uv tool warnings
61856193 ensure_local_bin_in_path ()
61866194
6195+ # Track download failures across all steps for final error reporting
6196+ download_failures : list [str ] = []
6197+
61876198 # Step 3: Download/copy custom files
61886199 print ()
61896200 print (f'{ Colors .CYAN } Step 3: Processing file downloads...{ Colors .NC } ' )
61906201 files_to_download = config .get ('files-to-download' , [])
61916202 if files_to_download :
6192- process_file_downloads (files_to_download , config_source , base_url , args .auth )
6203+ if not process_file_downloads (files_to_download , config_source , base_url , args .auth ):
6204+ download_failures .append ('file downloads' )
61936205 else :
61946206 info ('No custom files to download' )
61956207
@@ -6217,13 +6229,21 @@ def main() -> None:
62176229 print ()
62186230 print (f'{ Colors .CYAN } Step 7: Processing agents...{ Colors .NC } ' )
62196231 agents = config .get ('agents' , [])
6220- process_resources (agents , agents_dir , 'agents' , config_source , base_url , args .auth )
6232+ if agents :
6233+ if not process_resources (agents , agents_dir , 'agents' , config_source , base_url , args .auth ):
6234+ download_failures .append ('agents' )
6235+ else :
6236+ info ('No agents to process' )
62216237
62226238 # Step 8: Process slash commands
62236239 print ()
62246240 print (f'{ Colors .CYAN } Step 8: Processing slash commands...{ Colors .NC } ' )
62256241 commands = config .get ('slash-commands' , [])
6226- process_resources (commands , commands_dir , 'slash commands' , config_source , base_url , args .auth )
6242+ if commands :
6243+ if not process_resources (commands , commands_dir , 'slash commands' , config_source , base_url , args .auth ):
6244+ download_failures .append ('slash commands' )
6245+ else :
6246+ info ('No slash commands to process' )
62276247
62286248 # Step 9: Process skills
62296249 print ()
@@ -6235,7 +6255,11 @@ def main() -> None:
62356255 if isinstance (skills_raw , list )
62366256 else []
62376257 )
6238- process_skills (skills , skills_dir , config_source , args .auth )
6258+ if skills :
6259+ if not process_skills (skills , skills_dir , config_source , args .auth ):
6260+ download_failures .append ('skills' )
6261+ else :
6262+ info ('No skills configured' )
62396263
62406264 # Step 10: Process system prompt (if specified)
62416265 print ()
@@ -6246,7 +6270,8 @@ def main() -> None:
62466270 clean_prompt = system_prompt .split ('?' )[0 ] if '?' in system_prompt else system_prompt
62476271 sys_prompt_filename = Path (clean_prompt ).name
62486272 prompt_path = prompts_dir / sys_prompt_filename
6249- handle_resource (system_prompt , prompt_path , config_source , base_url , args .auth )
6273+ if not handle_resource (system_prompt , prompt_path , config_source , base_url , args .auth ):
6274+ download_failures .append ('system prompt' )
62506275 else :
62516276 info ('No additional system prompt configured' )
62526277
@@ -6303,7 +6328,8 @@ def main() -> None:
63036328 print ()
63046329 print (f'{ Colors .CYAN } Step 13: Downloading hooks...{ Colors .NC } ' )
63056330 hooks = config .get ('hooks' , {})
6306- download_hook_files (hooks , claude_user_dir , config_source , base_url , args .auth )
6331+ if not download_hook_files (hooks , claude_user_dir , config_source , base_url , args .auth ):
6332+ download_failures .append ('hook files' )
63076333
63086334 # Step 14: Configure settings
63096335 print ()
@@ -6357,7 +6383,33 @@ def main() -> None:
63576383 info ('Environment configuration completed successfully' )
63586384 info ('To create custom commands, add "command-names: [name1, name2]" to your config' )
63596385
6360- # Final message
6386+ # Check for download failures and report accordingly
6387+ if download_failures :
6388+ print ()
6389+ print (f'{ Colors .RED } ========================================================================{ Colors .NC } ' )
6390+ print (f'{ Colors .RED } Setup Completed with Errors{ Colors .NC } ' )
6391+ print (f'{ Colors .RED } ========================================================================{ Colors .NC } ' )
6392+ print ()
6393+ error ('The following resources failed to download:' )
6394+ for failure in download_failures :
6395+ error (f' - { failure } ' )
6396+ print ()
6397+ error ('Configuration steps were completed, but some files are missing.' )
6398+ error ('Please check your network connection and authentication, then re-run the setup.' )
6399+ print ()
6400+
6401+ # If running elevated via UAC, add a pause so user can see the error
6402+ if was_elevated_via_uac and not is_running_in_pytest ():
6403+ print ()
6404+ print (f'{ Colors .RED } ========================================================================{ Colors .NC } ' )
6405+ print (f'{ Colors .RED } Setup Completed with Download Errors{ Colors .NC } ' )
6406+ print (f'{ Colors .RED } ========================================================================{ Colors .NC } ' )
6407+ print ()
6408+ input ('Press Enter to exit...' )
6409+
6410+ sys .exit (1 )
6411+
6412+ # Final message - success (no download failures)
63616413 print ()
63626414 print (f'{ Colors .GREEN } ========================================================================{ Colors .NC } ' )
63636415 print (f'{ Colors .GREEN } Setup Complete!{ Colors .NC } ' )
0 commit comments