← Back to Blog

Designing the pyrpc dev setup wizard: two questions, zero friction

·8 min read

Most CLIs that need configuration make you answer ten questions before they let you work. pyRPC’s first-run wizard asks the minimum that cannot be guessed, and guesses everything else. The result: a pyrpc.json that appears after two prompts and never asks again.

Question one: the entry module

The first question is the one thing pyRPC genuinely cannot infer: which Python module contains your @rpc procedures and your mount_fastapi/mount_flaskcall? The wizard does narrow it down, though. It checks the conventional filenames , main.py, server.py, app.py, app/main.py, and pre-fills the first hit:

default_module = "main"
for candidate in ["main.py", "server.py", "app.py", "app/main.py"]:
 if (Path(root) / candidate).exists():
 default_module = candidate.replace(".py", "").replace("/", ".")
 break

module = questionary.text(
 "Entry module",
 default=default_module,
 instruction="(e.g. main, app.server, the file that calls mount_fastapi/mount_flask)",
).ask()

Accepting the default is one keystroke. The candidate list encodes the platform’s conventions, so the prompt is almost always pre-answered correctly before you see it.

Question two: where do your clients live?

The second question has three shapes depending on what the tree-walk found. That’s the interesting part, the wizard branches on evidence before it asks:

detected_projects = _find_frontend_projects(root)

if not detected_projects:
 # ask client root + framework directly
 ...
if len(detected_projects) == 1:
 # pre-fill client root and framework from the detection
 ...
# multiple: list them, then select or enter manually
  • Nothing detected, asks for a client root (default .) and a framework from the menu (Next.js, Nuxt, Svelte, Vite, Astro, Other).
  • Exactly one project detected, both fields come pre-filled from the detection; you confirm and move on.
  • Several projects detected, prints the list, then lets you multi-select the ones to wire up, writing clients and framework: "Mixed".

In every branch the number of new prompts stays tiny, because detection does the work of memory. The wizard’s job is confirmation, not interrogation.

Runs once, by construction

The wizard only fires when there is no pyrpc.json to read. In dev, the config lookup happens first; the wizard is the fallback:

cfg_path = _find_config()

elif cfg_path is None or reconfigure:
 cfg = _run_wizard(cwd)
 cfg_path = _write_config(cfg)
 console.print(f" [green]✓[/green] pyrpc.json created")
else:
 with open(cfg_path) as f:
 cfg = json.load(f)

The second run reads the file, asks nothing, and starts the server. And when the wizard has to run again, via --reconfigure, it always writes a fresh config rather than merging, so re-running setup never leaves half-updated state behind.

Design principles

  • Ask only what cannot be guessed. Framework and client root are detectable from config files; the entry module is a guess you confirm. Never prompt for answers the filesystem already knows.
  • Pre-fill from convention. The default is always the most likely answer, so the fast path is Enter × 2.
  • Write once, read forever. The config is durable, committed, and re-read by dev, watch, and codegen, the wizard is a one-time cost.
  • Graceful exits. None from any prompt (Ctrl+C) raises typer.Exit(code=0), canceling setup is a clean no-op, not an error.

The wizard exists to disappear. After the first run it is a line in a config file, and the--yes flag can skip even that.

Read the full changelogfor the complete list of changes.