172 lines
5.2 KiB
Python
172 lines
5.2 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
spr init — CLI room scaffolding wizard
|
|
|
|
Interactive terminal wizard that walks through layers 0-6,
|
|
generating cfg/<room>/ for a new soleprint room.
|
|
|
|
Usage:
|
|
python -m init.cli myroom # Interactive wizard
|
|
python -m init.cli myroom --from sample # Clone existing room as variant
|
|
"""
|
|
|
|
import argparse
|
|
import logging
|
|
import sys
|
|
|
|
from init.core import (
|
|
CFG_DIR,
|
|
BACKEND_FRAMEWORKS,
|
|
FRONTEND_FRAMEWORKS,
|
|
clone_room,
|
|
generate_layer0,
|
|
generate_layer1,
|
|
generate_layer2,
|
|
generate_layer3,
|
|
generate_layer4,
|
|
generate_layer5,
|
|
generate_layer6,
|
|
next_free_port,
|
|
)
|
|
|
|
log = logging.getLogger("init")
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Terminal prompts
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def ask(prompt: str, default: str = "") -> str:
|
|
try:
|
|
if default:
|
|
val = input(f"{prompt} [{default}]: ").strip()
|
|
return val or default
|
|
return input(f"{prompt}: ").strip()
|
|
except EOFError:
|
|
return default or ""
|
|
|
|
|
|
def ask_yn(prompt: str, default: bool = True) -> bool:
|
|
suffix = "[Y/n]" if default else "[y/N]"
|
|
try:
|
|
val = input(f"{prompt} {suffix}: ").strip().lower()
|
|
except EOFError:
|
|
return default
|
|
if not val:
|
|
return default
|
|
return val in ("y", "yes")
|
|
|
|
|
|
def ask_choice(prompt: str, choices: list[str], default: str = "") -> str:
|
|
choices_str = "/".join(choices)
|
|
try:
|
|
if default:
|
|
val = input(f"{prompt} [{choices_str}] ({default}): ").strip().lower()
|
|
return val if val in choices else default
|
|
while True:
|
|
val = input(f"{prompt} [{choices_str}]: ").strip().lower()
|
|
if val in choices:
|
|
return val
|
|
log.warning(" Choose one of: %s", choices_str)
|
|
except EOFError:
|
|
return default or choices[0]
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Wizard
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def wizard(room: str):
|
|
room_dir = CFG_DIR / room
|
|
|
|
if room_dir.exists():
|
|
log.error("'%s' already exists in cfg/", room)
|
|
sys.exit(1)
|
|
|
|
port = next_free_port()
|
|
|
|
log.info("\n=== Soleprint Room Setup: %s ===", room)
|
|
log.info(" Target: cfg/%s/", room)
|
|
|
|
room_type = ask_choice("Room type?", ["standalone", "managed"], "standalone")
|
|
port = int(ask("Hub port", str(port)))
|
|
is_managed = room_type == "managed"
|
|
|
|
# Layer 0: always
|
|
config = generate_layer0(room_dir, room, port, is_managed)
|
|
|
|
# Layer 1: docker
|
|
if ask_yn("\nContinue to Layer 1 (Docker)?"):
|
|
generate_layer1(room_dir, room, port)
|
|
else:
|
|
log.info("Done! Build with: python build.py --cfg %s", room)
|
|
return
|
|
|
|
app_info = None
|
|
|
|
# Layer 2: managed app
|
|
if is_managed:
|
|
if ask_yn("\nContinue to Layer 2 (Managed App)?"):
|
|
app_name = ask("App name", room)
|
|
backend_path = ask("Backend repo path")
|
|
backend_framework = ask_choice("Backend framework?", BACKEND_FRAMEWORKS, "django")
|
|
frontend_path = ask("Frontend repo path [skip]")
|
|
has_frontend = bool(frontend_path) and frontend_path.lower() != "skip"
|
|
|
|
frontend_framework = None
|
|
if has_frontend:
|
|
frontend_framework = ask_choice("Frontend framework?", FRONTEND_FRAMEWORKS, "nextjs")
|
|
|
|
app_info = generate_layer2(
|
|
room_dir, room, config,
|
|
app_name, backend_path, backend_framework,
|
|
frontend_path if has_frontend else None,
|
|
frontend_framework,
|
|
)
|
|
else:
|
|
log.info("Done! Build with: python build.py --cfg %s", room)
|
|
return
|
|
|
|
# Layer 3: link
|
|
if ask_yn("\nContinue to Layer 3 (Link - DB Bridge)?", default=False):
|
|
generate_layer3(room_dir, room, app_info["backend_framework"])
|
|
|
|
# Layer 4: scripts
|
|
app_name = app_info["app_name"] if app_info else None
|
|
if ask_yn("\nContinue to Layer 4 (Scripts)?"):
|
|
generate_layer4(room_dir, room, app_name)
|
|
|
|
# Layer 5: tester
|
|
if ask_yn("\nContinue to Layer 5 (Test Suite)?", default=False):
|
|
url = ask("Test target URL", "http://localhost:8000")
|
|
generate_layer5(room_dir, room, url)
|
|
|
|
# Layer 6: nginx (only if managed with frontend)
|
|
if is_managed and app_info and app_info["has_frontend"]:
|
|
if ask_yn("\nContinue to Layer 6 (Nginx Sidebar Injection)?", default=False):
|
|
generate_layer6(room_dir, room, app_info["app_name"])
|
|
|
|
log.info("Ready! Build with: python build.py --cfg %s", room)
|
|
|
|
|
|
def main():
|
|
logging.basicConfig(level=logging.INFO, format="%(message)s")
|
|
|
|
parser = argparse.ArgumentParser(
|
|
description="Soleprint room scaffolding wizard",
|
|
usage="python -m init.cli <room> [--from <source>]",
|
|
)
|
|
parser.add_argument("room", help="Name of the new room")
|
|
parser.add_argument("--from", dest="from_room", help="Clone from existing room")
|
|
|
|
args = parser.parse_args()
|
|
|
|
if args.from_room:
|
|
clone_room(args.from_room, args.room)
|
|
else:
|
|
wizard(args.room)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|