88from typing import List , Optional , Set
99from importlib_resources import files , as_file
1010
11+ from xircuits .utils .pathing import resolve_working_dir , components_base_dir
1112from .core_libs import is_core_library
1213
1314from xircuits .utils .pathing import (
@@ -51,6 +52,7 @@ def update_library(
5152 dry_run : bool = False ,
5253 prune : bool = False ,
5354 install_deps : bool = True ,
55+ use_latest : bool = False ,
5456) -> str :
5557 """
5658 Update an installed component library (core or regular).
@@ -67,6 +69,7 @@ def update_library(
6769 dry_run: If True, compute actions and print a unified diff (no files changed).
6870 prune: If True, also archive local-only files/dirs (rename to *.bak).
6971 install_deps: If True (default), update per-library extra and install its requirements.
72+ use_latest: If True, ignore metadata ref and pull latest from default branch.
7073
7174 Returns:
7275 Summary message of update results.
@@ -104,6 +107,7 @@ def update_library(
104107 dry_run = dry_run ,
105108 prune = prune ,
106109 install_deps = install_deps ,
110+ use_latest = use_latest ,
107111 )
108112
109113def _update_from_wheel (
@@ -200,6 +204,7 @@ def _update_from_git(
200204 dry_run : bool ,
201205 prune : bool ,
202206 install_deps : bool ,
207+ use_latest : bool = False ,
203208) -> str :
204209 """
205210 Update a regular component library from its git repository.
@@ -213,7 +218,7 @@ def _update_from_git(
213218
214219 timestamp = time .strftime ("%Y%m%d-%H%M%S" )
215220
216- source_spec = _resolve_source_spec (lib_name , repo , ref )
221+ source_spec = _resolve_source_spec (lib_name , repo , ref , use_latest )
217222 if not source_spec or not source_spec .repo_url :
218223 raise RuntimeError (
219224 f"Could not resolve a repository URL for '{ lib_name } '. "
@@ -382,20 +387,29 @@ def _resolve_source_spec(
382387 lib_name : str ,
383388 repo_override : Optional [str ],
384389 user_ref : Optional [str ],
390+ use_latest : bool = False ,
385391) -> Optional [SourceSpec ]:
386392 """
387393 Priority:
388394 0) explicit repo override (CLI/API 'repo=')
389395 1) pyproject.toml [tool.xircuits.components] entry (source + tag/rev)
390396 2) manifest index via get_remote_config()
397+
398+ If use_latest=True, ignore metadata ref and use user_ref (or None for default branch).
391399 """
392400 # use repo url if specified
393401 if repo_override :
394402 return SourceSpec (repo_url = repo_override , desired_ref = user_ref )
395403
396404 # pyproject metadata
397405 source_url , meta_ref = read_component_metadata_entry (lib_name )
398- desired_ref = user_ref or meta_ref
406+
407+ # Determine ref: use_latest bypasses metadata ref
408+ if use_latest :
409+ desired_ref = user_ref # None means default branch
410+ else :
411+ desired_ref = user_ref or meta_ref
412+
399413 if source_url :
400414 return SourceSpec (repo_url = source_url , desired_ref = desired_ref )
401415
@@ -674,3 +688,190 @@ def _build_combined_diff(
674688 out .append (diff )
675689
676690 return "\n " .join (out )
691+
692+ # ---------- Update All functionality ----------
693+
694+ def update_all_libraries (
695+ dry_run : bool = False ,
696+ prune : bool = False ,
697+ install_deps : bool = True ,
698+ core_only : bool = False ,
699+ remote_only : bool = False ,
700+ exclude : List [str ] = None ,
701+ respect_refs : bool = False ,
702+ ) -> dict :
703+ """
704+ Update all installed component libraries found in xai_components/.
705+
706+ Args:
707+ dry_run: Preview changes without modifying files
708+ prune: Remove local-only files during update
709+ install_deps: Install/update Python dependencies
710+ core_only: Only update core libraries
711+ remote_only: Only update non-core libraries
712+ exclude: List of library names to skip
713+ respect_refs: Honor pinned refs in metadata (default: pull latest)
714+
715+ Returns:
716+ Dict with 'success', 'failed', 'skipped' lists and summary stats
717+ """
718+
719+ # Validate conflicting flags
720+ if core_only and remote_only :
721+ raise ValueError ("Cannot specify both --core-only and --remote-only" )
722+
723+ working_dir = resolve_working_dir ()
724+ if working_dir is None :
725+ raise RuntimeError ("Xircuits working directory not found. Run 'xircuits init' first." )
726+
727+ exclude_set = set ((exclude or []))
728+ exclude_set = {normalize_library_slug (x ) for x in exclude_set }
729+
730+ results = {
731+ "success" : [],
732+ "failed" : [],
733+ "skipped" : []
734+ }
735+
736+ # Discover libraries to update
737+ libraries_to_update = _discover_updateable_libraries (
738+ working_dir = working_dir ,
739+ core_only = core_only ,
740+ remote_only = remote_only ,
741+ exclude = exclude_set
742+ )
743+
744+ if not libraries_to_update :
745+ print ("No libraries found to update." )
746+ return results
747+
748+ print (f"Found { len (libraries_to_update )} { 'library' if len (libraries_to_update ) == 1 else 'libraries' } to update" )
749+ if dry_run :
750+ print ("DRY-RUN MODE: No files will be modified\n " )
751+ print ()
752+
753+ # Update each library
754+ for lib_name in sorted (libraries_to_update ):
755+ try :
756+ print (f"{ '=' * 60 } " )
757+ print (f"Updating: { lib_name } " )
758+ print (f"{ '=' * 60 } " )
759+
760+ # For --all without --respect-refs, pull latest by setting use_latest=True
761+ message = update_library (
762+ library_name = lib_name ,
763+ repo = None ,
764+ ref = None ,
765+ dry_run = dry_run ,
766+ prune = prune ,
767+ install_deps = install_deps ,
768+ use_latest = not respect_refs ,
769+ )
770+
771+ results ["success" ].append ((lib_name , message ))
772+ print (f"✓ { lib_name } : { message } \n " )
773+
774+ except Exception as e :
775+ error_msg = str (e )
776+ results ["failed" ].append ((lib_name , error_msg ))
777+ print (f"✗ { lib_name } : Failed - { error_msg } \n " )
778+ # Continue to next library
779+
780+ # Print summary
781+ _print_update_all_summary (results , dry_run )
782+
783+ return results
784+
785+ def _discover_updateable_libraries (
786+ working_dir : Path ,
787+ core_only : bool ,
788+ remote_only : bool ,
789+ exclude : set
790+ ) -> List [str ]:
791+ """
792+ Scan xai_components directory and return list of updateable library names.
793+ """
794+
795+ base_dir = components_base_dir (working_dir )
796+ if not base_dir .exists ():
797+ return []
798+
799+ libraries = []
800+
801+ # Check base.py
802+ base_py = base_dir / "base.py"
803+ if base_py .exists () and base_py .is_file ():
804+ if not remote_only and "base.py" not in exclude :
805+ if core_only or not remote_only :
806+ libraries .append ("base.py" )
807+
808+ # Scan xai_* directories
809+ for item in base_dir .glob ("xai_*" ):
810+ if not item .is_dir ():
811+ continue
812+
813+ # Must have __init__.py to be valid
814+ if not (item / "__init__.py" ).exists ():
815+ continue
816+
817+ lib_name = item .name
818+
819+ # Check exclusions
820+ if lib_name in exclude :
821+ continue
822+
823+ # Check core/remote filters
824+ is_core = is_core_library (lib_name )
825+ if core_only and not is_core :
826+ continue
827+ if remote_only and is_core :
828+ continue
829+
830+ libraries .append (lib_name )
831+
832+ return libraries
833+
834+
835+ def _print_update_all_summary (results : dict , dry_run : bool ):
836+ """
837+ Print a formatted summary of update results.
838+ """
839+ print ()
840+ print ("=" * 60 )
841+ print ("Update All Summary" )
842+ print ("=" * 60 )
843+ print ()
844+
845+ if results ["success" ]:
846+ print ("✓ SUCCEEDED:" )
847+ for lib_name , message in results ["success" ]:
848+ print (f" { lib_name :20} { message } " )
849+ print ()
850+
851+ if results ["failed" ]:
852+ print ("✗ FAILED:" )
853+ for lib_name , error in results ["failed" ]:
854+ # Truncate long errors
855+ error_display = error if len (error ) <= 60 else error [:57 ] + "..."
856+ print (f" { lib_name :20} { error_display } " )
857+ print ()
858+
859+ if results ["skipped" ]:
860+ print ("⊘ SKIPPED:" )
861+ for lib_name , reason in results ["skipped" ]:
862+ print (f" { lib_name :20} { reason } " )
863+ print ()
864+
865+ # Summary counts
866+ total = len (results ["success" ]) + len (results ["failed" ]) + len (results ["skipped" ])
867+ summary_parts = []
868+ if results ["success" ]:
869+ summary_parts .append (f"{ len (results ['success' ])} succeeded" )
870+ if results ["failed" ]:
871+ summary_parts .append (f"{ len (results ['failed' ])} failed" )
872+ if results ["skipped" ]:
873+ summary_parts .append (f"{ len (results ['skipped' ])} skipped" )
874+
875+ mode_suffix = " (dry-run)" if dry_run else ""
876+ print (f"{ ', ' .join (summary_parts )} { mode_suffix } " )
877+ print ("=" * 60 )
0 commit comments