@@ -830,20 +830,44 @@ def init(force: bool):
830830
831831@cli .command ()
832832@click .option ("--config" , "config_file" , default = None , type = click .Path (), help = "Path to .oikb.yaml (default: ./.oikb.yaml)." )
833- def validate (config_file : str | None ):
834- """Validate .oikb.yaml without running anything."""
833+ @click .option ("--deep" , is_flag = True , help = "Verify connectivity: ping Open WebUI, check API key, confirm each KB exists." )
834+ def validate (config_file : str | None , deep : bool ):
835+ """Validate .oikb.yaml without running anything.
836+
837+ By default, checks YAML structure and source syntax.
838+ With --deep, also verifies the Open WebUI API is reachable,
839+ the API key is valid, and each Knowledge Base ID exists.
840+ """
835841 if config_file :
836842 import yaml
837843 with open (config_file ) as f :
838844 data = yaml .safe_load (f )
845+ data = _interpolate_env (data ) if data else data
839846 entries = (data .get ("sources" ) or data .get ("sync" , [])) if data else []
847+ defaults = data .get ("defaults" , {}) if data else {}
848+ if defaults and entries :
849+ entries = [_deep_merge (defaults , e ) for e in entries ]
840850 else :
841851 entries = _load_oikb_yaml ()
842852
843853 if not entries :
844854 click .echo (click .style ("No .oikb.yaml found or file is empty." , fg = "red" ), err = True )
845855 sys .exit (1 )
846856
857+ # Deep validation: create a client and verify connectivity first.
858+ client = None
859+ if deep :
860+ click .echo (click .style ("Deep validation\n " , bold = True ))
861+ try :
862+ client = _make_client (
863+ url = entries [0 ].get ("url" ),
864+ token = entries [0 ].get ("token" ),
865+ )
866+ click .echo (click .style (" ✓ API reachable" , fg = "green" ) + f" { client ._http .base_url } " )
867+ except Exception as e :
868+ click .echo (click .style (f" ✗ Cannot reach Open WebUI: { e } " , fg = "red" ))
869+ sys .exit (1 )
870+
847871 has_errors = False
848872 for i , entry in enumerate (entries , 1 ):
849873 entry_name = entry .get ("name" , f"entry #{ i } " )
@@ -855,13 +879,32 @@ def validate(config_file: str | None):
855879 has_errors = True
856880 continue
857881
858- # Try resolving the connector to catch bad source strings .
882+ # Syntax check: resolve the connector.
859883 try :
860884 _resolve_connector (source )
861- click .echo (click .style (f" ✓ { entry_name } " , fg = "green" ) + f" { source } → { kb_id } " )
862885 except Exception as e :
863886 click .echo (click .style (f" ✗ { entry_name } : { e } " , fg = "red" ))
864887 has_errors = True
888+ continue
889+
890+ # Deep check: verify the KB exists.
891+ if deep and client :
892+ try :
893+ kb = client .get_kb (kb_id )
894+ kb_name = kb .get ("name" , "?" )
895+ file_count = len (kb .get ("files" , []))
896+ click .echo (
897+ click .style (f" ✓ { entry_name } " , fg = "green" )
898+ + f" { source } → { kb_name } ({ file_count } files)"
899+ )
900+ except Exception as e :
901+ click .echo (click .style (f" ✗ { entry_name } : KB { kb_id } — { e } " , fg = "red" ))
902+ has_errors = True
903+ else :
904+ click .echo (click .style (f" ✓ { entry_name } " , fg = "green" ) + f" { source } → { kb_id } " )
905+
906+ if client :
907+ client .close ()
865908
866909 if has_errors :
867910 sys .exit (1 )
0 commit comments