33
44from __future__ import annotations
55
6+ """Click-based CLI frontend for PyCompiler ARK."""
7+
68from pathlib import Path
79import sys
810
1416 render_checks_text ,
1517 render_workspace_init_result ,
1618)
19+ from .click_workspace_commands import register_workspace_commands
20+ from .click_quality_commands import register_quality_commands
1721from .dedicated import _run_bcasl_headless , run_dedicated_cli
1822from .headless_ops import (
1923 bcasl_doctor_payload ,
4953
5054
5155def has_click () -> bool :
56+ """Return ``True`` when the optional Click dependency is available."""
5257 return click is not None
5358
5459
5560def _echo_payload (payload , as_json : bool = False ) -> None :
61+ """Print payload as JSON or plain text depending on ``as_json``."""
5662 if as_json :
5763 text = emit_json (payload )
5864 sys .stdout .write (text )
5965 if not text .endswith ("\n " ):
6066 sys .stdout .write ("\n " )
6167 sys .stdout .flush ()
6268 return
69+ # Sortie texte standardisée: aucune logique métier ici, uniquement rendu.
6370 if isinstance (payload , str ):
6471 plain (payload )
6572 else :
6673 plain (str (payload ))
6774
6875
6976def _emit_and_exit (payload , code : int , as_json : bool = False ) -> None :
77+ """Emit payload and exit immediately with the provided code."""
7078 _echo_payload (payload , as_json = as_json )
7179 raise click .exceptions .Exit (code )
7280
7381
7482def _run_workspace_init_with_progress (workspace_dir : str , with_venv : bool = False ):
83+ """Run workspace init and render progress with Rich when available."""
7584 try :
7685 from rich .console import Console # type: ignore
7786 from rich .status import Status # type: ignore
7887 except Exception :
7988 Console = None
8089 Status = None
8190
91+ # Fallback propre si Rich n'est pas disponible.
8292 if Console is None or Status is None :
8393 payload = workspace_init_payload (workspace_dir , with_venv = with_venv )
8494 for item in payload .get ("steps" , []):
@@ -113,10 +123,13 @@ def _cb(_step: str, _status: str, message: str) -> None:
113123
114124
115125def _resolve_workspace_path (workspace : str | None ) -> str | None :
126+ """Normalize optional workspace path input."""
116127 return normalize_path (workspace )
117128
118129
119130def _workspace_init_emit (workspace_dir : str , as_json : bool , with_venv : bool ) -> None :
131+ """Run init workflow and emit output according to CLI output mode."""
132+ # Mode JSON destiné à l'automatisation (CI, scripts, wrappers).
120133 if as_json :
121134 payload = workspace_init_payload (workspace_dir , with_venv = with_venv )
122135 if not payload .get ("ok" ):
@@ -132,7 +145,9 @@ def _workspace_init_emit(workspace_dir: str, as_json: bool, with_venv: bool) ->
132145def _workspace_config_auto_emit (
133146 workspace_dir : str , entrypoint : str | None , as_json : bool
134147) -> None :
148+ """Run auto-config workflow and emit output according to CLI output mode."""
135149 payload = workspace_config_auto_payload (workspace_dir , entrypoint = entrypoint )
150+ # Même contrat de sortie que pour `init`: JSON machine-friendly ou texte humain.
136151 if as_json :
137152 if not payload .get ("ok" ):
138153 _emit_and_exit (payload , EXIT_WORKSPACE_INVALID , as_json = True )
@@ -150,6 +165,8 @@ def _workspace_config_auto_emit(
150165
151166
152167def _ensure_workspace_exists (workspace_dir : str | None ) -> str | None :
168+ """Ensure workspace directory exists before opening GUI sub-apps."""
169+ # Création opportuniste pour éviter un échec GUI si le dossier est attendu mais absent.
153170 if not workspace_dir :
154171 return None
155172 ws_path = Path (workspace_dir )
@@ -166,9 +183,11 @@ def _ensure_workspace_exists(workspace_dir: str | None) -> str | None:
166183
167184
168185def build_cli (app_version : str ):
186+ """Build and return the root Click command group."""
169187 if click is None :
170188 raise RuntimeError ("Click is not available" )
171189
190+ # Groupe racine: options globales + dispatch vers sous-commandes.
172191 @click .group (
173192 invoke_without_command = True ,
174193 context_settings = dict (help_option_names = ["-h" , "--help" ]),
@@ -262,6 +281,7 @@ def cli(
262281 )
263282 )
264283
284+ # Commandes GUI explicites.
265285 @cli .group ()
266286 def gui ():
267287 """Launch graphical interfaces."""
@@ -289,6 +309,7 @@ def gui_bcasl(workspace):
289309 def gui_engines (workspace ):
290310 sys .exit (launch_engines_gui (_ensure_workspace_exists (_resolve_workspace_path (workspace ))))
291311
312+ # Commandes moteur headless et d'inspection.
292313 @cli .group ()
293314 def engine ():
294315 """Inspect and run compilation engines."""
@@ -479,186 +500,33 @@ def engine_compile(engine_id, file_path, workspace, as_json):
479500 else :
480501 raise click .ClickException (result .get ("error" , "Compilation failed" ))
481502
482- @cli .group ()
483- def workspace ():
484- """Inspect workspace state and configuration."""
485-
486- @workspace .command ("inspect" )
487- @click .argument ("path" , required = False , type = click .Path (exists = False ))
488- @click .option ("--json" , "as_json" , is_flag = True )
489- @click .option ("--strict" , is_flag = True , help = "Exit non-zero when the workspace is invalid" )
490- def workspace_inspect (path , as_json , strict ):
491- payload = workspace_inspect_payload (_resolve_workspace_path (path or "." ))
492- if as_json :
493- if strict and not payload .get ("exists" ):
494- _emit_and_exit (payload , EXIT_WORKSPACE_INVALID , as_json = True )
495- _echo_payload (payload , as_json = True )
496- return
497- if not payload .get ("exists" ):
498- raise click .ClickException (payload .get ("error" , "Workspace not found" ))
499- plain (f"Workspace: { payload ['workspace' ]} " )
500- plain (f" Entrypoint: { payload .get ('entrypoint' ) or '(none)' } " )
501- plain (f" Python files: { payload ['python_file_count' ]} " )
502- plain (
503- " Requirements files: "
504- + (", " .join (payload ["requirements_files_found" ]) or "none" )
505- )
506-
507- @workspace .command ("entrypoint" )
508- @click .argument ("path" , required = False , type = click .Path (exists = False ))
509- @click .option ("--json" , "as_json" , is_flag = True )
510- @click .option ("--strict" , is_flag = True , help = "Exit non-zero when no entrypoint is resolved" )
511- def workspace_entrypoint (path , as_json , strict ):
512- payload = workspace_inspect_payload (_resolve_workspace_path (path or "." ))
513- result = {"workspace" : payload .get ("workspace" ), "entrypoint" : payload .get ("entrypoint" )}
514- if as_json :
515- if strict and not result .get ("entrypoint" ):
516- _emit_and_exit (result , EXIT_PRECHECK_FAILED , as_json = True )
517- _echo_payload (result , as_json = True )
518- return
519- plain (result ["entrypoint" ] or "" )
520- if strict and not result .get ("entrypoint" ):
521- raise click .exceptions .Exit (EXIT_PRECHECK_FAILED )
522-
523- @workspace .command ("files" )
524- @click .argument ("path" , required = False , type = click .Path (exists = False ))
525- @click .option ("--json" , "as_json" , is_flag = True )
526- def workspace_files (path , as_json ):
527- payload = workspace_inspect_payload (_resolve_workspace_path (path or "." ))
528- result = {
529- "workspace" : payload .get ("workspace" ),
530- "python_file_count" : payload .get ("python_file_count" , 0 ),
531- "python_files_preview" : payload .get ("python_files_preview" , []),
532- }
533- if as_json :
534- _echo_payload (result , as_json = True )
535- return
536- plain (f"Python files: { result ['python_file_count' ]} " )
537- for item in result ["python_files_preview" ]:
538- plain (f" - { item } " )
539-
540- @cli .command ("init" )
541- @click .argument ("workspace" , required = False , type = click .Path (exists = False ))
542- @click .option ("--json" , "as_json" , is_flag = True )
543- @click .option ("--with-venv" , is_flag = True , help = "Create or reuse a local workspace venv" )
544- def init_cmd (workspace , as_json , with_venv ):
545- workspace_dir = _resolve_workspace_path (workspace or "." )
546- _workspace_init_emit (workspace_dir , as_json = as_json , with_venv = with_venv )
547-
548- @cli .command ("config-auto" )
549- @click .argument ("workspace" , required = False , type = click .Path (exists = False ))
550- @click .option ("--entrypoint" , type = str , help = "Override detected entrypoint" )
551- @click .option ("--json" , "as_json" , is_flag = True )
552- def config_auto_cmd (workspace , entrypoint , as_json ):
553- _workspace_config_auto_emit (
554- _resolve_workspace_path (workspace or "." ),
555- entrypoint = entrypoint ,
556- as_json = as_json ,
557- )
558-
559- @cli .command ("cfg-auto" )
560- @click .argument ("workspace" , required = False , type = click .Path (exists = False ))
561- @click .option ("--entrypoint" , type = str , help = "Override detected entrypoint" )
562- @click .option ("--json" , "as_json" , is_flag = True )
563- def cfg_auto_cmd (workspace , entrypoint , as_json ):
564- _workspace_config_auto_emit (
565- _resolve_workspace_path (workspace or "." ),
566- entrypoint = entrypoint ,
567- as_json = as_json ,
568- )
569-
570- @cli .group ("ws" )
571- def ws ():
572- """Alias for workspace bootstrap commands."""
573-
574- @ws .command ("init" )
575- @click .argument ("workspace" , required = False , type = click .Path (exists = False ))
576- @click .option ("--json" , "as_json" , is_flag = True )
577- @click .option ("--with-venv" , is_flag = True , help = "Create or reuse a local workspace venv" )
578- def ws_init_cmd (workspace , as_json , with_venv ):
579- workspace_dir = _resolve_workspace_path (workspace or "." )
580- _workspace_init_emit (workspace_dir , as_json = as_json , with_venv = with_venv )
581-
582- @ws .command ("config-auto" )
583- @click .argument ("workspace" , required = False , type = click .Path (exists = False ))
584- @click .option ("--entrypoint" , type = str , help = "Override detected entrypoint" )
585- @click .option ("--json" , "as_json" , is_flag = True )
586- def ws_config_auto_cmd (workspace , entrypoint , as_json ):
587- _workspace_config_auto_emit (
588- _resolve_workspace_path (workspace or "." ),
589- entrypoint = entrypoint ,
590- as_json = as_json ,
591- )
592-
593- @cli .command ("doctor" )
594- @click .argument ("workspace" , required = False , type = click .Path (exists = False ))
595- @click .option ("--json" , "as_json" , is_flag = True )
596- @click .option ("--strict" , is_flag = True , help = "Exit non-zero when diagnostics detect issues" )
597- def doctor (workspace , as_json , strict ):
598- payload = doctor_payload (workspace = _resolve_workspace_path (workspace ))
599- if strict :
600- workspace_payload = payload .get ("workspace" )
601- has_workspace_issue = isinstance (workspace_payload , dict ) and not workspace_payload .get ("exists" , True )
602- has_engine_issue = payload ["engines" ]["compatible_count" ] != payload ["engines" ]["count" ]
603- has_qt_issue = not payload .get ("qt_available" , False )
604- strict_failed = bool (has_workspace_issue or has_engine_issue or has_qt_issue )
605- else :
606- strict_failed = False
607- if as_json :
608- if strict_failed :
609- _emit_and_exit (payload , EXIT_PRECHECK_FAILED , as_json = True )
610- _echo_payload (payload , as_json = True )
611- return
612- plain ("PyCompiler ARK Doctor" )
613- plain (f" Python: { payload ['platform' ]['python' ]} " )
614- plain (f" Platform: { payload ['platform' ]['system' ]} { payload ['platform' ]['release' ]} " )
615- plain (f" Qt available: { 'yes' if payload ['qt_available' ] else 'no' } " )
616- plain (
617- f" Engines: { payload ['engines' ]['compatible_count' ]} /{ payload ['engines' ]['count' ]} compatible"
618- )
619- if strict_failed :
620- raise click .exceptions .Exit (EXIT_PRECHECK_FAILED )
621-
622- @cli .command ("check" )
623- @click .argument ("workspace" , required = False , type = click .Path (exists = False ))
624- @click .option ("--json" , "as_json" , is_flag = True )
625- @click .option (
626- "--strict/--no-strict" ,
627- default = True ,
628- show_default = True ,
629- help = "Exit non-zero when checks fail" ,
503+ # Enregistrement modulaire des commandes workspace.
504+ register_workspace_commands (
505+ cli = cli ,
506+ click = click ,
507+ resolve_workspace_path = _resolve_workspace_path ,
508+ emit_and_exit = _emit_and_exit ,
509+ echo_payload = _echo_payload ,
510+ workspace_init_emit = _workspace_init_emit ,
511+ workspace_config_auto_emit = _workspace_config_auto_emit ,
512+ workspace_inspect_payload = workspace_inspect_payload ,
630513 )
631- @click .option (
632- "--require-entrypoint/--no-require-entrypoint" ,
633- default = True ,
634- show_default = True ,
635- help = "Require workspace entrypoint in checks" ,
636- )
637- @click .option (
638- "--fail-only/--all-checks" ,
639- default = True ,
640- show_default = True ,
641- help = "Display only failing checks in text output" ,
514+
515+ # Enregistrement modulaire des commandes qualité (doctor/check).
516+ register_quality_commands (
517+ cli = cli ,
518+ click = click ,
519+ resolve_workspace_path = _resolve_workspace_path ,
520+ emit_and_exit = _emit_and_exit ,
521+ echo_payload = _echo_payload ,
522+ doctor_payload = doctor_payload ,
523+ ci_smoke_payload = lambda workspace = None , require_entrypoint = False : ci_smoke_payload (
524+ workspace = workspace , require_entrypoint = require_entrypoint
525+ ),
526+ render_checks_text = render_checks_text ,
642527 )
643- def check_cmd (workspace , as_json , strict , require_entrypoint , fail_only ):
644- """Single-command CI/CD gate with strict defaults."""
645- payload = ci_smoke_payload (
646- workspace = _resolve_workspace_path (workspace ),
647- require_entrypoint = require_entrypoint ,
648- )
649- if as_json :
650- if strict and not payload .get ("ok" ):
651- _emit_and_exit (payload , EXIT_PRECHECK_FAILED , as_json = True )
652- _echo_payload (payload , as_json = True )
653- return
654- render_checks_text (
655- "PyCompiler ARK Check" ,
656- list (payload .get ("checks" , [])),
657- fail_only = fail_only ,
658- )
659- if strict and not payload .get ("ok" ):
660- raise click .exceptions .Exit (EXIT_PRECHECK_FAILED )
661528
529+ # Génération de templates de départ.
662530 @cli .group ()
663531 def scaffold ():
664532 """Generate starter templates."""
@@ -691,6 +559,7 @@ def scaffold_plugin_cmd(name, root, as_json):
691559 else :
692560 raise click .ClickException (payload .get ("reason" , "Unable to create scaffold" ))
693561
562+ # Espace BCASL (GUI + actions headless spécifiques).
694563 @cli .group (invoke_without_command = True )
695564 @click .pass_context
696565 def bcasl (ctx ):
@@ -759,7 +628,7 @@ def unload_cmd(as_json):
759628 else :
760629 raise click .ClickException (result ["message" ])
761630
762- # Backward-compatible aliases
631+ # Alias rétro-compatibles pour ne pas casser les usages historiques.
763632 @cli .command (context_settings = dict (help_option_names = ["-h" , "--help" ]))
764633 @click .argument ("workspace" , required = False , type = click .Path (exists = False ))
765634 def engines (workspace ):
0 commit comments