1717 openadapt health
1818 openadapt cleanup
1919 openadapt config
20+ openadapt doctor
2021"""
2122
2223from __future__ import annotations
2324
2425import argparse
26+ import os
2527import sys
2628import types
2729from pathlib import Path
@@ -287,6 +289,108 @@ def cmd_config(args: argparse.Namespace, engine: types.SimpleNamespace) -> None:
287289 print (engine .config .model_dump_json (indent = 2 ))
288290
289291
292+ def cmd_doctor (args : argparse .Namespace , engine : types .SimpleNamespace ) -> None :
293+ """Check system dependencies and configuration."""
294+ from engine import __version__
295+
296+ checks : list [tuple [str , bool , str ]] = []
297+
298+ # Engine version
299+ checks .append (("Engine version" , True , f"v{ __version__ } " ))
300+
301+ # Python version
302+ py_ver = f"{ sys .version_info .major } .{ sys .version_info .minor } .{ sys .version_info .micro } "
303+ py_ok = sys .version_info >= (3 , 11 )
304+ checks .append (("Python" , py_ok , py_ver if py_ok else f"{ py_ver } (need >=3.11)" ))
305+
306+ # Data directory
307+ data_ok = engine .config .data_dir .exists () and os .access (engine .config .data_dir , os .W_OK )
308+ checks .append (("Data directory" , data_ok , str (engine .config .data_dir )))
309+
310+ # Database
311+ try :
312+ engine .db .conn .execute ("SELECT 1" ).fetchone ()
313+ checks .append (("Database (SQLite)" , True , "connected" ))
314+ except Exception as e :
315+ checks .append (("Database (SQLite)" , False , str (e )))
316+
317+ # openadapt-capture
318+ try :
319+ import openadapt_capture
320+ ver = getattr (openadapt_capture , "__version__" , "installed" )
321+ checks .append (("openadapt-capture" , True , ver ))
322+ except ImportError :
323+ checks .append (("openadapt-capture" , False , "not installed (recording disabled)" ))
324+
325+ # openadapt-privacy
326+ try :
327+ import openadapt_privacy
328+ ver = getattr (openadapt_privacy , "__version__" , "installed" )
329+ checks .append (("openadapt-privacy" , True , ver ))
330+ except ImportError :
331+ checks .append (("openadapt-privacy" , False , "not installed (advanced scrubbing disabled)" ))
332+
333+ # psutil
334+ try :
335+ import psutil
336+ checks .append (("psutil" , True , psutil .__version__ ))
337+ except ImportError :
338+ checks .append (("psutil" , False , "not installed (health monitoring disabled)" ))
339+
340+ # boto3 (optional)
341+ try :
342+ import boto3
343+ checks .append (("boto3 (S3 backend)" , True , boto3 .__version__ ))
344+ except ImportError :
345+ checks .append (("boto3 (S3 backend)" , False ,
346+ "not installed (pip install openadapt-desktop[enterprise])" ))
347+
348+ # huggingface_hub (optional)
349+ try :
350+ import huggingface_hub
351+ checks .append (("huggingface_hub (HF backend)" , True , huggingface_hub .__version__ ))
352+ except ImportError :
353+ checks .append (("huggingface_hub (HF backend)" , False ,
354+ "not installed (pip install openadapt-desktop[community])" ))
355+
356+ # magic-wormhole
357+ import shutil
358+ wormhole_path = shutil .which ("wormhole" )
359+ checks .append ((
360+ "magic-wormhole (P2P backend)" ,
361+ wormhole_path is not None ,
362+ wormhole_path or "not found (pip install magic-wormhole)" ,
363+ ))
364+
365+ # Storage mode
366+ checks .append (("Storage mode" , True , engine .config .storage_mode ))
367+
368+ # S3 credentials (if configured)
369+ if engine .config .s3_bucket :
370+ has_creds = bool (engine .config .s3_access_key_id and engine .config .s3_secret_access_key )
371+ detail = f"bucket={ engine .config .s3_bucket } " if has_creds else "bucket set but keys missing"
372+ checks .append (("S3 credentials" , has_creds , detail ))
373+
374+ # HF token (if configured)
375+ if engine .config .hf_token :
376+ checks .append (("HuggingFace token" , True , f"repo={ engine .config .hf_repo } " ))
377+
378+ # Print results
379+ print ("OpenAdapt Doctor" )
380+ print ("=" * 60 )
381+ ok_count = sum (1 for _ , ok , _ in checks if ok )
382+ for name , ok , detail in checks :
383+ marker = "OK" if ok else "!!"
384+ print (f" [{ marker } ] { name } : { detail } " )
385+
386+ print ("=" * 60 )
387+ total = len (checks )
388+ print (f"{ ok_count } /{ total } checks passed" )
389+
390+ if ok_count < total :
391+ print ("\n Run 'pip install openadapt-desktop[full]' to install all optional dependencies." )
392+
393+
290394_COMMANDS = {
291395 "record" : cmd_record ,
292396 "list" : cmd_list ,
@@ -301,6 +405,7 @@ def cmd_config(args: argparse.Namespace, engine: types.SimpleNamespace) -> None:
301405 "health" : cmd_health ,
302406 "cleanup" : cmd_cleanup ,
303407 "config" : cmd_config ,
408+ "doctor" : cmd_doctor ,
304409}
305410
306411
@@ -359,6 +464,9 @@ def main(argv: list[str] | None = None) -> None:
359464 # config
360465 subparsers .add_parser ("config" , help = "Show configuration" )
361466
467+ # doctor
468+ subparsers .add_parser ("doctor" , help = "Check dependencies and configuration" )
469+
362470 args = parser .parse_args (argv )
363471
364472 config = EngineConfig ()
0 commit comments