wizard
This commit is contained in:
171
init/cli.py
Normal file
171
init/cli.py
Normal file
@@ -0,0 +1,171 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user