replica-omnisciente/scripts/onboard-client.py
Raphael Cautus (Maestro) 2f26f2d836 feat(scripts): onboarding, GWS, lifestream, muscriptor, music, data-sharing
- onboard-client.py: client replica scaffolding CLI
- gws/: Google Workspace sync (Gmail, Calendar, Drive)
- lifestream/: life event stream collector
- muscriptor-mcp/: audio → MIDI MCP server
- music-mcp/: music library MCP server
- data_sharing/: consent-gated data sharing (Python + TS)
- sync-mirrors.py: GitHub → Forgejo mirror engine
- brain-to-gbrain.py, vault-sync.py, test-all.sh
- shared/: TS data-sharing library + index
- dirac: provider registry update
- .gitignore: exclude Rust build artifacts

Co-authored-by: Álvaro de Campos <campos@portugalfuturista.org>
2026-07-31 14:58:02 +01:00

283 lines
10 KiB
Python

#!/usr/bin/env python3
"""
Réplica Omnisciente — Client Onboarding Tool
Scaffolds a new client's complete replica-omnisciente: brain (.aurelio/),
realms/, scripts/, CI/CD, identity, providers, connectors — the full power set.
USAGE (interactive):
python3 scripts/onboard-client.py
USAGE (CLI flags):
python3 scripts/onboard-client.py \\
--client-name "Acme Corp" \\
--git-url https://github.com/acme/replica \\
--lead-engineer "Jane Doe" \\
--realm iot-backend --realm-name "IoT Backend" --realm-repo https://github.com/acme/iot
USAGE (JSON config):
python3 scripts/onboard-client.py --config onboarding.json
USAGE (dry run — preview without writing):
python3 scripts/onboard-client.py --client-name "Acme Corp" --dry-run
"""
from __future__ import annotations
import argparse
import json
import os
import sys
from pathlib import Path
# Bootstrap: ensure we can import the onboarding package
SCRIPTS_DIR = Path(__file__).resolve().parent
sys.path.insert(0, str(SCRIPTS_DIR))
from onboarding import OnboardingConfig, scaffold_client
from onboarding.config import _slugify
def _prompt(label: str, default: str = "") -> str:
"""Prompt with default, return user input or default."""
suffix = f" [{default}]" if default else ""
val = input(f"{label}{suffix}: ").strip()
return val or default
def _prompt_bool(label: str, default: bool = True) -> bool:
d = "Y/n" if default else "y/N"
val = input(f"{label} [{d}]: ").strip().lower()
if not val:
return default
return val in ("y", "yes", "true", "1")
def _prompt_list(label: str, fields: list[str]) -> list[dict]:
"""Prompt for a list of items (e.g. team members, realms)."""
items = []
print(f"\n{label} (press Enter with empty name to finish):")
while True:
first_field = fields[0]
val = input(f" {first_field.capitalize()}: ").strip()
if not val:
break
item = {first_field: val}
for f in fields[1:]:
item[f] = input(f" {f.capitalize()}: ").strip()
items.append(item)
print()
return items
def interactive_wizard() -> OnboardingConfig:
"""Run the interactive onboarding wizard."""
print()
print("=" * 60)
print(" RÉPLICA OMNISCIENTE — CLIENT ONBOARDING")
print("=" * 60)
print()
# ── Identity ──
print("── Identity ──")
client_name = _prompt("Client name")
slug = _slugify(client_name)
client_slug = _prompt("Client slug", slug)
description = _prompt("Description (one line)")
# ── Git ──
print("\n── Git Repository ──")
git_url = _prompt("Git URL (leave empty for local-only)")
forge_remote = _prompt("Remote name", "origin")
# ── Infrastructure ──
print("\n── Infrastructure ──")
sync_endpoint = _prompt("Sync endpoint URL (leave empty for local-only)")
proxmox_host = _prompt("Proxmox host IP (leave empty if none)")
ct_id = 0
if proxmox_host:
ct_id = int(_prompt("Proxmox CT ID", "0") or "0")
# ── Team ──
print("\n── Lead Engineer ──")
lead_name = _prompt("Lead engineer name")
lead_email = _prompt("Lead engineer email")
print("\n── Team Members ──")
team = _prompt_list("Add team members", ["name", "email", "role"])
# ── Realms ──
print("\n── Initial Realms ──")
realms_raw = _prompt_list("Add realms", ["slug", "name", "repo", "stack", "description"])
realms = []
for r in realms_raw:
realms.append({
"slug": r.get("slug", ""),
"name": r.get("name", ""),
"repo": r.get("repo", ""),
"stack": r.get("stack", ""),
"description": r.get("description", ""),
})
# ── Models ──
print("\n── Models ──")
local_model = _prompt("Default local model", "qwen2.5-coder:14b")
cloud_model = _prompt("Default cloud model", "gemini-2.5-pro")
ollama_url = _prompt("Ollama URL", "http://127.0.0.1:11434")
# ── Powers ──
print("\n── Capabilities to include ──")
print("(These are the 'powers' of Réplica Omnisciente to copy)")
include_skills = _prompt_bool("Copy skills tree (CLI guides)?", True)
include_providers = _prompt_bool("Include provider registry?", True)
include_connectors = _prompt_bool("Include connector registry?", True)
include_scripts = _prompt_bool("Copy scripts (sync, importers)?", True)
include_ci = _prompt_bool("Generate CI/CD workflows?", True)
return OnboardingConfig(
client_name=client_name,
client_slug=client_slug,
description=description,
git_url=git_url,
forge_remote_name=forge_remote,
sync_endpoint=sync_endpoint,
proxmox_host=proxmox_host,
ct_id=ct_id,
lead_engineer_name=lead_name,
lead_engineer_email=lead_email,
team_members=team,
realms=realms,
default_local_model=local_model,
default_cloud_model=cloud_model,
ollama_url=ollama_url,
include_skills=include_skills,
include_providers=include_providers,
include_connectors=include_connectors,
include_scripts=include_scripts,
include_ci=include_ci,
)
def build_from_args(args: argparse.Namespace) -> OnboardingConfig:
"""Build config from CLI flags (non-interactive mode)."""
realms = []
if args.realm:
for i, slug in enumerate(args.realm):
realms.append({
"slug": slug,
"name": args.realm_name[i] if i < len(args.realm_name) else slug,
"repo": args.realm_repo[i] if i < len(args.realm_repo) else "",
"stack": args.realm_stack[i] if i < len(args.realm_stack) else "",
"description": args.realm_desc[i] if i < len(args.realm_desc) else "",
})
return OnboardingConfig(
client_name=args.client_name,
client_slug=args.client_slug or "",
description=args.description or "",
git_url=args.git_url or "",
forge_remote_name=args.remote_name or "origin",
sync_endpoint=args.sync_endpoint or "",
proxmox_host=args.proxmox_host or "",
ct_id=args.ct_id or 0,
lead_engineer_name=args.lead_engineer or "",
lead_engineer_email=args.lead_email or "",
realms=realms,
default_local_model=args.local_model or "qwen2.5-coder:14b",
default_cloud_model=args.cloud_model or "gemini-2.5-pro",
ollama_url=args.ollama_url or "http://127.0.0.1:11434",
include_skills=not args.no_skills,
include_providers=not args.no_providers,
include_connectors=not args.no_connectors,
include_scripts=not args.no_scripts,
include_ci=not args.no_ci,
output_dir=args.output or "",
dry_run=args.dry_run,
)
def main():
parser = argparse.ArgumentParser(
description="Scaffold a new client's Réplica Omnisciente",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=__doc__,
)
# ── Source ──
parser.add_argument("--config", help="JSON config file (skips all other flags)")
parser.add_argument("--interactive", "-i", action="store_true",
help="Force interactive wizard even with flags")
# ── Identity ──
parser.add_argument("--client-name", help="Client name")
parser.add_argument("--client-slug", help="Client slug (auto from name)")
parser.add_argument("--description", help="One-line description")
# ── Git ──
parser.add_argument("--git-url", help="Git remote URL")
parser.add_argument("--remote-name", default="origin", help="Git remote name")
parser.add_argument("--output", "-o", help="Output directory (default: ./<slug>)")
# ── Infrastructure ──
parser.add_argument("--sync-endpoint", help="MCP sync endpoint URL")
parser.add_argument("--proxmox-host", help="Proxmox host for brain sync")
parser.add_argument("--ct-id", type=int, help="Proxmox CT ID")
# ── Team ──
parser.add_argument("--lead-engineer", help="Lead engineer name")
parser.add_argument("--lead-email", help="Lead engineer email")
# ── Realms ──
parser.add_argument("--realm", action="append", default=[], help="Realm slug (repeatable)")
parser.add_argument("--realm-name", action="append", default=[])
parser.add_argument("--realm-repo", action="append", default=[])
parser.add_argument("--realm-stack", action="append", default=[])
parser.add_argument("--realm-desc", action="append", default=[])
# ── Models ──
parser.add_argument("--local-model", help="Default local model")
parser.add_argument("--cloud-model", help="Default cloud model")
parser.add_argument("--ollama-url", help="Ollama URL")
# ── Power toggles ──
parser.add_argument("--no-skills", action="store_true", help="Skip skills tree")
parser.add_argument("--no-providers", action="store_true", help="Skip provider registry")
parser.add_argument("--no-connectors", action="store_true", help="Skip connector registry")
parser.add_argument("--no-scripts", action="store_true", help="Skip scripts")
parser.add_argument("--no-ci", action="store_true", help="Skip CI/CD")
# ── Meta ──
parser.add_argument("--dry-run", action="store_true", help="Preview without writing")
parser.add_argument("--print-json", action="store_true",
help="Print resolved config as JSON and exit")
args = parser.parse_args()
# ── Resolve config ──
if args.config:
cfg = OnboardingConfig.from_json_file(args.config)
if args.dry_run:
cfg.dry_run = True
if args.output:
cfg.output_dir = args.output
elif args.interactive or not args.client_name:
cfg = interactive_wizard()
if args.dry_run:
cfg.dry_run = True
else:
cfg = build_from_args(args)
if args.print_json:
print(cfg.to_json())
return
# ── Execute ──
try:
scaffold_client(cfg)
except Exception as e:
print(f"\nERROR: {e}", file=sys.stderr)
sys.exit(1)
if __name__ == "__main__":
main()