Skip to content

Commit e5a5117

Browse files
committed
feat(cli): ajouter les commandes entrypoint, venv et config engine
1 parent d01b0c1 commit e5a5117

4 files changed

Lines changed: 725 additions & 0 deletions

File tree

cli/click_app.py

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
"""Click-based CLI frontend for PyCompiler ARK."""
77

8+
import json
89
from pathlib import Path
910
import sys
1011

@@ -26,13 +27,21 @@
2627
doctor_payload,
2728
emit_json,
2829
engine_config_path_payload,
30+
engine_config_reset_payload,
31+
engine_config_set_payload,
2932
engine_config_show_payload,
3033
engine_doctor_payload,
3134
engine_info_payload,
3235
engine_list_payload,
3336
scaffold_engine,
3437
scaffold_plugin,
38+
venv_install_requirements_payload,
39+
venv_status_payload,
40+
venv_use_system_payload,
41+
venv_use_venv_payload,
3542
workspace_config_auto_payload,
43+
workspace_entrypoint_clear_payload,
44+
workspace_entrypoint_set_payload,
3645
workspace_init_payload,
3746
workspace_inspect_payload,
3847
)
@@ -450,6 +459,72 @@ def engine_config_path(engine_id, workspace, as_json):
450459
raise click.ClickException("Workspace is required")
451460
plain(payload.get("path", ""))
452461

462+
@engine_config.command("set")
463+
@click.argument("engine_id")
464+
@click.option("-w", "--workspace", required=True, type=click.Path(exists=False))
465+
@click.option("--options-json", type=str, help="Inline JSON object with engine options")
466+
@click.option("--options-file", type=click.Path(exists=True), help="Path to JSON file with engine options")
467+
@click.option("--replace", is_flag=True, help="Replace options instead of merge")
468+
@click.option("--json", "as_json", is_flag=True)
469+
def engine_config_set(engine_id, workspace, options_json, options_file, replace, as_json):
470+
if bool(options_json) == bool(options_file):
471+
raise click.ClickException("Provide exactly one of --options-json or --options-file")
472+
try:
473+
if options_file:
474+
options = json.loads(Path(options_file).read_text(encoding="utf-8"))
475+
else:
476+
options = json.loads(options_json)
477+
except Exception as exc:
478+
raise click.ClickException(f"Invalid JSON options: {exc}")
479+
if not isinstance(options, dict):
480+
raise click.ClickException("Engine options must be a JSON object")
481+
payload = engine_config_set_payload(
482+
engine_id,
483+
workspace=_resolve_workspace_path(workspace),
484+
options=options,
485+
merge=not bool(replace),
486+
)
487+
if as_json:
488+
if payload.get("error") == "workspace is required":
489+
_emit_and_exit(payload, EXIT_WORKSPACE_INVALID, as_json=True)
490+
if payload.get("found") is False:
491+
_emit_and_exit(payload, EXIT_ENGINE_NOT_FOUND, as_json=True)
492+
if not payload.get("saved"):
493+
_emit_and_exit(payload, EXIT_PRECHECK_FAILED, as_json=True)
494+
_echo_payload(payload, as_json=True)
495+
return
496+
if payload.get("found") is False:
497+
raise click.ClickException(f"Engine not found: {engine_id}")
498+
if not payload.get("saved"):
499+
raise click.ClickException(payload.get("error", "Unable to save engine config"))
500+
success(f"Engine config saved: {engine_id}")
501+
plain(f" Path: {payload.get('path')}")
502+
503+
@engine_config.command("reset")
504+
@click.argument("engine_id")
505+
@click.option("-w", "--workspace", required=True, type=click.Path(exists=False))
506+
@click.option("--json", "as_json", is_flag=True)
507+
def engine_config_reset(engine_id, workspace, as_json):
508+
payload = engine_config_reset_payload(
509+
engine_id,
510+
workspace=_resolve_workspace_path(workspace),
511+
)
512+
if as_json:
513+
if payload.get("error") == "workspace is required":
514+
_emit_and_exit(payload, EXIT_WORKSPACE_INVALID, as_json=True)
515+
if payload.get("found") is False:
516+
_emit_and_exit(payload, EXIT_ENGINE_NOT_FOUND, as_json=True)
517+
if not payload.get("reset"):
518+
_emit_and_exit(payload, EXIT_PRECHECK_FAILED, as_json=True)
519+
_echo_payload(payload, as_json=True)
520+
return
521+
if payload.get("found") is False:
522+
raise click.ClickException(f"Engine not found: {engine_id}")
523+
if not payload.get("reset"):
524+
raise click.ClickException(payload.get("error", "Unable to reset engine config"))
525+
success(f"Engine config reset: {engine_id}")
526+
plain(f" Path: {payload.get('path')}")
527+
453528
@engine.command("dry-run")
454529
@click.argument("engine_id")
455530
@click.argument("file_path", type=click.Path(exists=True))
@@ -500,6 +575,83 @@ def engine_compile(engine_id, file_path, workspace, as_json):
500575
else:
501576
raise click.ClickException(result.get("error", "Compilation failed"))
502577

578+
@cli.group()
579+
def venv():
580+
"""Manage workspace virtual environment preferences."""
581+
582+
@venv.command("status")
583+
@click.argument("workspace", required=False, type=click.Path(exists=False))
584+
@click.option("--json", "as_json", is_flag=True)
585+
def venv_status(workspace, as_json):
586+
payload = venv_status_payload(_resolve_workspace_path(workspace or "."))
587+
if as_json:
588+
if not payload.get("ok"):
589+
_emit_and_exit(payload, EXIT_WORKSPACE_INVALID, as_json=True)
590+
_echo_payload(payload, as_json=True)
591+
return
592+
if not payload.get("ok"):
593+
raise click.ClickException(payload.get("error", "Unable to inspect venv status"))
594+
plain(f"Workspace: {payload.get('workspace')}")
595+
plain(f" Mode: {payload.get('mode')}")
596+
plain(f" Venv: {payload.get('venv_path') or '(none)'}")
597+
plain(f" Pref file: {payload.get('pref_path')}")
598+
599+
@venv.command("use-system")
600+
@click.argument("workspace", required=False, type=click.Path(exists=False))
601+
@click.option("--json", "as_json", is_flag=True)
602+
def venv_use_system(workspace, as_json):
603+
payload = venv_use_system_payload(_resolve_workspace_path(workspace or "."))
604+
if as_json:
605+
if not payload.get("ok"):
606+
_emit_and_exit(payload, EXIT_WORKSPACE_INVALID, as_json=True)
607+
_echo_payload(payload, as_json=True)
608+
return
609+
if not payload.get("ok"):
610+
raise click.ClickException(payload.get("error", "Unable to set system python mode"))
611+
success("Workspace venv mode set to system")
612+
613+
@venv.command("use-venv")
614+
@click.argument("workspace", required=False, type=click.Path(exists=False))
615+
@click.argument("venv_path", required=False, type=click.Path(exists=False))
616+
@click.option("--create", is_flag=True, help="Create .venv when no venv path is provided")
617+
@click.option("--json", "as_json", is_flag=True)
618+
def venv_use_venv(workspace, venv_path, create, as_json):
619+
payload = venv_use_venv_payload(
620+
_resolve_workspace_path(workspace or "."),
621+
venv_path=venv_path,
622+
create_if_missing=bool(create),
623+
)
624+
if as_json:
625+
if not payload.get("ok"):
626+
_emit_and_exit(payload, EXIT_PRECHECK_FAILED, as_json=True)
627+
_echo_payload(payload, as_json=True)
628+
return
629+
if not payload.get("ok"):
630+
raise click.ClickException(payload.get("error", "Unable to set venv mode"))
631+
success("Workspace venv mode set to venv")
632+
plain(f" Venv: {payload.get('venv_path')}")
633+
634+
@venv.command("install-req")
635+
@click.argument("workspace", required=False, type=click.Path(exists=False))
636+
@click.option("--force-pip", is_flag=True, help="Force pip-based install mode")
637+
@click.option("--json", "as_json", is_flag=True)
638+
def venv_install_req(workspace, force_pip, as_json):
639+
payload = venv_install_requirements_payload(
640+
_resolve_workspace_path(workspace or "."),
641+
force_pip=bool(force_pip),
642+
)
643+
if as_json:
644+
if not payload.get("ok"):
645+
_emit_and_exit(payload, EXIT_PRECHECK_FAILED, as_json=True)
646+
_echo_payload(payload, as_json=True)
647+
return
648+
if not payload.get("ok"):
649+
raise click.ClickException(payload.get("error", "Requirements installation failed"))
650+
if payload.get("installed") is False:
651+
info(payload.get("reason", "No requirements installation needed"))
652+
return
653+
success("Requirements installed")
654+
503655
# Enregistrement modulaire des commandes workspace.
504656
register_workspace_commands(
505657
cli=cli,
@@ -510,6 +662,8 @@ def engine_compile(engine_id, file_path, workspace, as_json):
510662
workspace_init_emit=_workspace_init_emit,
511663
workspace_config_auto_emit=_workspace_config_auto_emit,
512664
workspace_inspect_payload=workspace_inspect_payload,
665+
workspace_entrypoint_set_payload=workspace_entrypoint_set_payload,
666+
workspace_entrypoint_clear_payload=workspace_entrypoint_clear_payload,
513667
)
514668

515669
# Enregistrement modulaire des commandes qualité (doctor/check).

cli/click_workspace_commands.py

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ def register_workspace_commands(
2323
workspace_init_emit: Callable[[str, bool, bool], None],
2424
workspace_config_auto_emit: Callable[[str, str | None, bool], None],
2525
workspace_inspect_payload: Callable[[str], dict[str, object]],
26+
workspace_entrypoint_set_payload: Callable[[str, str | None], dict[str, object]],
27+
workspace_entrypoint_clear_payload: Callable[[str], dict[str, object]],
2628
) -> None:
2729
"""Register workspace and workspace-bootstrap command groups on a Click root."""
2830
# Cette fonction isole un bloc volumineux de click_app pour améliorer la maintenabilité.
@@ -69,6 +71,48 @@ def workspace_entrypoint(path, as_json, strict):
6971
if strict and not result.get("entrypoint"):
7072
raise click.exceptions.Exit(EXIT_PRECHECK_FAILED)
7173

74+
@workspace.command("entrypoint-set")
75+
@click.argument("path", required=False, type=click.Path(exists=False))
76+
@click.argument("entrypoint", required=True, type=str)
77+
@click.option("--json", "as_json", is_flag=True)
78+
def workspace_entrypoint_set(path, entrypoint, as_json):
79+
payload = workspace_entrypoint_set_payload(
80+
resolve_workspace_path(path or "."),
81+
entrypoint,
82+
)
83+
if as_json:
84+
if not payload.get("ok"):
85+
code = (
86+
EXIT_WORKSPACE_INVALID
87+
if payload.get("error") in {"workspace is required", "workspace not found"}
88+
else EXIT_PRECHECK_FAILED
89+
)
90+
emit_and_exit(payload, code, True)
91+
echo_payload(payload, True)
92+
return
93+
if not payload.get("ok"):
94+
raise click.ClickException(payload.get("error", "Unable to set entrypoint"))
95+
plain(payload.get("entrypoint") or "")
96+
97+
@workspace.command("entrypoint-clear")
98+
@click.argument("path", required=False, type=click.Path(exists=False))
99+
@click.option("--json", "as_json", is_flag=True)
100+
def workspace_entrypoint_clear(path, as_json):
101+
payload = workspace_entrypoint_clear_payload(resolve_workspace_path(path or "."))
102+
if as_json:
103+
if not payload.get("ok"):
104+
code = (
105+
EXIT_WORKSPACE_INVALID
106+
if payload.get("error") in {"workspace is required", "workspace not found"}
107+
else EXIT_PRECHECK_FAILED
108+
)
109+
emit_and_exit(payload, code, True)
110+
echo_payload(payload, True)
111+
return
112+
if not payload.get("ok"):
113+
raise click.ClickException(payload.get("error", "Unable to clear entrypoint"))
114+
plain("")
115+
72116
@workspace.command("files")
73117
@click.argument("path", required=False, type=click.Path(exists=False))
74118
@click.option("--json", "as_json", is_flag=True)
@@ -221,6 +265,48 @@ def ws_config_auto_cmd(workspace, entrypoint, as_json):
221265
as_json=as_json,
222266
)
223267

268+
@ws.command("entrypoint-set")
269+
@click.argument("workspace", required=False, type=click.Path(exists=False))
270+
@click.argument("entrypoint", required=True, type=str)
271+
@click.option("--json", "as_json", is_flag=True)
272+
def ws_entrypoint_set_cmd(workspace, entrypoint, as_json):
273+
payload = workspace_entrypoint_set_payload(
274+
resolve_workspace_path(workspace or "."),
275+
entrypoint,
276+
)
277+
if as_json:
278+
if not payload.get("ok"):
279+
code = (
280+
EXIT_WORKSPACE_INVALID
281+
if payload.get("error") in {"workspace is required", "workspace not found"}
282+
else EXIT_PRECHECK_FAILED
283+
)
284+
emit_and_exit(payload, code, True)
285+
echo_payload(payload, True)
286+
return
287+
if not payload.get("ok"):
288+
raise click.ClickException(payload.get("error", "Unable to set entrypoint"))
289+
plain(payload.get("entrypoint") or "")
290+
291+
@ws.command("entrypoint-clear")
292+
@click.argument("workspace", required=False, type=click.Path(exists=False))
293+
@click.option("--json", "as_json", is_flag=True)
294+
def ws_entrypoint_clear_cmd(workspace, as_json):
295+
payload = workspace_entrypoint_clear_payload(resolve_workspace_path(workspace or "."))
296+
if as_json:
297+
if not payload.get("ok"):
298+
code = (
299+
EXIT_WORKSPACE_INVALID
300+
if payload.get("error") in {"workspace is required", "workspace not found"}
301+
else EXIT_PRECHECK_FAILED
302+
)
303+
emit_and_exit(payload, code, True)
304+
echo_payload(payload, True)
305+
return
306+
if not payload.get("ok"):
307+
raise click.ClickException(payload.get("error", "Unable to clear entrypoint"))
308+
plain("")
309+
224310
@ws.command("apply")
225311
@click.argument("workspace", required=False, type=click.Path(exists=False))
226312
@click.option("--json", "as_json", is_flag=True)

0 commit comments

Comments
 (0)