Skip to content

Commit 4b1e9d7

Browse files
committed
feat(cli): renforcer la documentation interne et commentaires fr
1 parent 2d8dd8d commit 4b1e9d7

9 files changed

Lines changed: 501 additions & 352 deletions

cli/click_app.py

Lines changed: 47 additions & 178 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33

44
from __future__ import annotations
55

6+
"""Click-based CLI frontend for PyCompiler ARK."""
7+
68
from pathlib import Path
79
import sys
810

@@ -14,6 +16,8 @@
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
1721
from .dedicated import _run_bcasl_headless, run_dedicated_cli
1822
from .headless_ops import (
1923
bcasl_doctor_payload,
@@ -49,36 +53,42 @@
4953

5054

5155
def has_click() -> bool:
56+
"""Return ``True`` when the optional Click dependency is available."""
5257
return click is not None
5358

5459

5560
def _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

6976
def _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

7482
def _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

115125
def _resolve_workspace_path(workspace: str | None) -> str | None:
126+
"""Normalize optional workspace path input."""
116127
return normalize_path(workspace)
117128

118129

119130
def _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) ->
132145
def _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

152167
def _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

168185
def 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

Comments
 (0)