#!/usr/bin/env python3 """Publiciste Agent · orchestrateur CLI (scaffold Sprint 2). Cible de portage sur le VPS : `otoia/capabilities/publiciste.py` (cf. 03_agents/publiciste/AGENT.md · GAP_ANALYSIS_SPRINT1.md §3.13). Maintient le site public `vente.otov7.com` À PARTIR DES FAISABILITÉS CANONIQUES (data_room/PXX/), sans édition manuelle. Zéro invention de chiffre (CLAUDE.md #6). Sous-commandes : parse data_room/ -> projets_master.json (validé contre le schéma) validate projets_master.json generate projets_master.json -> index.html run data_room/ -> projets_master.json + index.html (pipeline complet) Pipeline (AGENT.md §Architecture technique) : data_room/PXX/ ──► parser ──► projets_master.json ──► generator ──► index.html ⚠ Ce worker n'écrit JAMAIS sur le VPS. La commande `run` produit les artefacts dans un dossier de sortie local ; le déploiement réel (copie vers /opt/oto/sites/vente/ + cache-bust + WhatsApp) est un livrable ultérieur exécuté côté serveur par l'agent (Semaines 4-5). """ from __future__ import annotations import argparse import json import os import sys sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) from lib import generator, parser, validator # noqa: E402 _SCHEMA_DIR = os.path.normpath( os.path.join(os.path.dirname(__file__), "..", "faisabilite") ) def _load_schema(name: str) -> dict: with open(os.path.join(_SCHEMA_DIR, name), encoding="utf-8") as fh: return json.load(fh) def _eprint(*args) -> None: print(*args, file=sys.stderr) # --------------------------------------------------------------------------- # def cmd_parse(ns: argparse.Namespace) -> int: master = parser.build_master(ns.data_room, generated_at=ns.generated_at) errors = validator.validate(master, _load_schema("projets_master.schema.json")) if errors: _eprint("❌ projets_master.json NON conforme au schéma :") for e in errors: _eprint(" ·", e) # On écrit quand même la sortie pour diagnostic si --force. if not ns.force: return 1 payload = json.dumps(master, ensure_ascii=False, indent=2) if ns.out: with open(ns.out, "w", encoding="utf-8") as fh: fh.write(payload + "\n") _eprint(f"✅ {len(master['projets'])} projet(s) → {ns.out}") else: print(payload) return 0 def cmd_validate(ns: argparse.Namespace) -> int: with open(ns.master, encoding="utf-8") as fh: master = json.load(fh) errors = validator.validate(master, _load_schema("projets_master.schema.json")) if errors: _eprint(f"❌ {ns.master} : {len(errors)} erreur(s)") for e in errors: _eprint(" ·", e) return 1 _eprint(f"✅ {ns.master} conforme ({len(master.get('projets', []))} projet(s)).") return 0 def cmd_generate(ns: argparse.Namespace) -> int: with open(ns.master, encoding="utf-8") as fh: master = json.load(fh) errors = validator.validate(master, _load_schema("projets_master.schema.json")) if errors and not ns.force: _eprint("❌ master non conforme — génération refusée (voir `validate`). --force pour outrepasser.") return 1 html_out = generator.render_site(master) if ns.out: with open(ns.out, "w", encoding="utf-8") as fh: fh.write(html_out) _eprint(f"✅ site généré → {ns.out} ({len(master.get('projets', []))} projet(s))") else: sys.stdout.write(html_out) return 0 def cmd_run(ns: argparse.Namespace) -> int: os.makedirs(ns.out_dir, exist_ok=True) master = parser.build_master(ns.data_room, generated_at=ns.generated_at) schema = _load_schema("projets_master.schema.json") errors = validator.validate(master, schema) master_path = os.path.join(ns.out_dir, "projets_master.json") with open(master_path, "w", encoding="utf-8") as fh: fh.write(json.dumps(master, ensure_ascii=False, indent=2) + "\n") if errors: _eprint(f"❌ master non conforme ({len(errors)} erreur(s)) — index NON généré.") for e in errors: _eprint(" ·", e) return 1 html_out = generator.render_site(master) index_path = os.path.join(ns.out_dir, "index.html") with open(index_path, "w", encoding="utf-8") as fh: fh.write(html_out) _eprint(f"✅ pipeline OK — {len(master['projets'])} projet(s)") _eprint(f" · {master_path}") _eprint(f" · {index_path}") return 0 # --------------------------------------------------------------------------- # def build_argparser() -> argparse.ArgumentParser: ap = argparse.ArgumentParser(prog="publiciste", description=__doc__.splitlines()[0]) sub = ap.add_subparsers(dest="cmd", required=True) p = sub.add_parser("parse", help="data_room/ -> projets_master.json") p.add_argument("data_room") p.add_argument("-o", "--out") p.add_argument("--generated-at", dest="generated_at", default=None) p.add_argument("--force", action="store_true", help="écrire même si non conforme") p.set_defaults(func=cmd_parse) p = sub.add_parser("validate", help="valide un projets_master.json") p.add_argument("master") p.set_defaults(func=cmd_validate) p = sub.add_parser("generate", help="projets_master.json -> index.html") p.add_argument("master") p.add_argument("-o", "--out") p.add_argument("--force", action="store_true") p.set_defaults(func=cmd_generate) p = sub.add_parser("run", help="pipeline complet data_room/ -> out_dir/") p.add_argument("data_room") p.add_argument("-o", "--out-dir", dest="out_dir", default="build") p.add_argument("--generated-at", dest="generated_at", default=None) p.set_defaults(func=cmd_run) return ap def main(argv: list[str] | None = None) -> int: ns = build_argparser().parse_args(argv) return ns.func(ns) if __name__ == "__main__": raise SystemExit(main())