"""Reflex CLI to create, run, and deploy apps.""" from __future__ import annotations from importlib.util import find_spec from pathlib import Path from typing import TYPE_CHECKING import click from reflex_base import constants from reflex_base.config import get_config from reflex_base.environment import environment from reflex_base.utils import console from reflex_cli.v2.deployments import hosting_cli from reflex.custom_components.custom_components import custom_components_cli if TYPE_CHECKING: from reflex_base.constants.base import LITERAL_ENV from reflex_cli.constants.base import LogLevel as HostingLogLevel def set_loglevel(ctx: click.Context, self: click.Parameter, value: str | None): """Set the log level. Args: ctx: The click context. self: The click command. value: The log level to set. """ if value is not None: loglevel = constants.LogLevel.from_string(value) console.set_log_level(loglevel) @click.group @click.version_option(constants.Reflex.VERSION, message="%(version)s") def cli(): """Reflex CLI to create, run, and deploy apps.""" loglevel_option = click.option( "--loglevel", "--log-level", "loglevel", type=click.Choice( [loglevel.value for loglevel in constants.LogLevel], case_sensitive=False, ), is_eager=True, callback=set_loglevel, expose_value=False, help="The log level to use.", ) def _init( name: str, template: str | None = None, ai: bool = False, ): """Initialize a new Reflex app in the given directory.""" from reflex.utils import exec, frontend_skeleton, prerequisites, templates # Show system info exec.output_system_info() if ai: from reflex.utils.redir import reflex_build_redirect reflex_build_redirect() return # Validate the app name. app_name = prerequisites.validate_app_name(name) console.rule(f"[bold]Initializing {app_name}") # Check prerequisites. prerequisites.check_latest_package_version(constants.Reflex.MODULE_NAME) prerequisites.initialize_reflex_user_directory() prerequisites.ensure_reflex_installation_id() # Set up the web project. prerequisites.initialize_frontend_dependencies() # Initialize the app. template = templates.initialize_app(app_name, template) # Initialize the .gitignore. frontend_skeleton.initialize_gitignore() template_msg = f" using the {template} template" if template else "" if Path(constants.PyprojectToml.FILE).exists(): needs_user_manual_update = False next_steps = " Run `uv run reflex run` to start the app." else: needs_user_manual_update = frontend_skeleton.initialize_requirements_txt() next_steps = " Install dependencies from `requirements.txt` with `uv pip install -r requirements.txt` (or your preferred installer) before running `uv run reflex run`." manual_update = ( f" Make sure to add `{constants.RequirementsTxt.DEFAULTS_STUB + constants.Reflex.VERSION}` to your requirements.txt file." if needs_user_manual_update else "" ) # Finish initializing the app. console.success(f"Initialized {app_name}{template_msg}.{manual_update}{next_steps}") @cli.command() @loglevel_option @click.option( "--name", metavar="APP_NAME", help="The name of the app to initialize.", ) @click.option( "--template", help="The template to initialize the app with.", ) @click.option( "--ai", is_flag=True, help="Use AI to create the initial template. Cannot be used with existing app or `--template` option.", ) def init( name: str, template: str | None, ai: bool, ): """Initialize a new Reflex app in the current directory.""" _init(name, template, ai) def _compile_app(*, avoid_dirty_check: bool = True): from reflex.utils import exec, prerequisites app_task = prerequisites.compile_or_validate_app args = (True,) kwargs = { "check_if_schema_up_to_date": True, "prerender_routes": exec.should_prerender_routes(), } # Granian fails if the app is already imported. if exec.should_use_granian() and avoid_dirty_check: import concurrent.futures with concurrent.futures.ProcessPoolExecutor(max_workers=1) as executor: compile_future = executor.submit(app_task, *args, **kwargs) return_result = compile_future.result() else: return_result = app_task(*args, **kwargs) if not return_result: raise SystemExit(1) def _run_dev( running_mode: constants.RunningMode, frontend_port: int | None, backend_port: int | None, backend_host: str, ): """Run the app in development mode.""" import atexit from reflex.utils import build, exec, processes, telemetry config = get_config() if frontend_port: config._set_persistent(frontend_port=frontend_port) if backend_port: config._set_persistent(backend_port=backend_port) if running_mode.has_frontend(): _compile_app() # Post a telemetry event. telemetry.send("run-dev") # Display custom message when there is a keyboard interrupt. atexit.register(processes.atexit_handler) # Run the frontend and backend together. commands = [] # Run the frontend on a separate thread. if running_mode.has_frontend(): build.setup_frontend(Path.cwd()) commands.append(( exec.run_frontend, Path.cwd(), frontend_port, running_mode.has_backend(), )) # Start the frontend and backend. with processes.run_concurrently_context(*commands): # In dev mode, run the backend on the main thread. if running_mode.has_backend() and backend_port: exec.run_backend( backend_host, int(backend_port), config.loglevel.subprocess_level(), running_mode.has_frontend(), ) # The windows uvicorn bug workaround # https://github.com/reflex-dev/reflex/issues/2335 if constants.IS_WINDOWS and exec.frontend_process: # Sends SIGTERM in windows exec.kill(exec.frontend_process.pid) def _run_prod(running_mode: constants.RunningMode, port: int, host: str): import atexit from reflex.utils import build, exec, processes, telemetry config = get_config() config._set_persistent(frontend_port=port, backend_port=port) if running_mode.has_frontend(): # Get the app module. _compile_app(avoid_dirty_check=False) build.setup_frontend_prod(Path.cwd()) _skip_compile() # Post a telemetry event. telemetry.send("run-prod") # Display custom message when there is a keyboard interrupt. atexit.register(processes.atexit_handler) exec.notify_app_running() exec.notify_frontend( f"http://{host}:{port}", backend_present=running_mode.has_backend(), ) if running_mode.has_backend(): exec.run_backend_prod( host, port, config.loglevel.subprocess_level(), running_mode.has_frontend() ) else: exec.run_frontend_prod(host, port) def _run( *, env: constants.Env = constants.Env.DEV, running_mode: constants.RunningMode = constants.RunningMode.FULLSTACK, frontend_port: int | None = None, backend_port: int | None = None, backend_host: str | None = None, ): """Run the app in the given directory.""" from reflex.istate.manager import reset_disk_state_manager from reflex.utils import exec, prerequisites, processes if frontend_port and not running_mode.has_frontend(): console.error("Cannot specify --frontend-port when not running frontend.") raise SystemExit(1) if backend_port and not running_mode.has_backend(): console.error("Cannot specify --backend-port when not running backend.") raise SystemExit(1) if ( env == constants.Env.PROD and frontend_port and backend_port and frontend_port != backend_port ): console.error("In production, frontend and backend must run on the same port.") raise SystemExit(1) config = get_config() backend_host = backend_host or config.backend_host # Set env mode in the environment environment.REFLEX_ENV_MODE.set(env) # Show system info exec.output_system_info() if running_mode == constants.RunningMode.BACKEND_ONLY: _skip_compile() prerequisites.assert_in_reflex_dir() # Check that the app is initialized. if running_mode.has_frontend() and prerequisites.needs_reinit(): _init(name=config.app_name) # Delete the states folder if it exists. reset_disk_state_manager() # Apply the new ports and host to the config. if frontend_port != config.frontend_port: config._set_persistent(frontend_port=frontend_port) if backend_port != config.backend_port: config._set_persistent(backend_port=backend_port) if backend_host != config.backend_host: config._set_persistent(backend_host=backend_host) # Reload the config to make sure the env vars are persistent. get_config(reload=True) console.rule("[bold]Starting Reflex App") prerequisites.check_latest_package_version(constants.Reflex.MODULE_NAME) if env == constants.Env.DEV: # Find the next available open port if applicable. if running_mode.has_frontend(): auto_increment_frontend = not bool(frontend_port or config.frontend_port) frontend_port = processes.handle_port( "frontend", ( frontend_port or config.frontend_port or constants.DefaultPorts.FRONTEND_PORT ), auto_increment=auto_increment_frontend, ) if running_mode.has_backend(): auto_increment_backend = not bool(backend_port or config.backend_port) backend_port = processes.handle_port( "backend", ( backend_port or config.backend_port or constants.DefaultPorts.BACKEND_PORT ), auto_increment=auto_increment_backend, ) _run_dev(running_mode, frontend_port, backend_port, backend_host) else: if running_mode == constants.RunningMode.BACKEND_ONLY: requested_port = backend_port or config.backend_port fallback_port = constants.DefaultPorts.BACKEND_PORT elif running_mode == constants.RunningMode.FRONTEND_ONLY: requested_port = frontend_port or config.frontend_port fallback_port = constants.DefaultPorts.FRONTEND_PORT else: requested_port = ( frontend_port or backend_port or config.frontend_port or config.backend_port ) fallback_port = constants.DefaultPorts.FRONTEND_PORT port = processes.handle_port( service_name=running_mode.name.lower(), port=requested_port or fallback_port, auto_increment=requested_port is None, ) _run_prod(running_mode, port, backend_host) @cli.command() @loglevel_option @click.option( "--env", type=click.Choice([e.value for e in constants.Env], case_sensitive=False), default=constants.Env.DEV.value, help="The environment to run the app in.", ) @click.option( "--frontend-only", is_flag=True, show_default=False, help="Execute only frontend.", envvar=environment.REFLEX_FRONTEND_ONLY.name, ) @click.option( "--backend-only", is_flag=True, show_default=False, help="Execute only backend.", envvar=environment.REFLEX_BACKEND_ONLY.name, ) @click.option( "--frontend-port", type=int, help="Specify a different frontend port.", envvar=environment.REFLEX_FRONTEND_PORT.name, ) @click.option( "--backend-port", type=int, help="Specify a different backend port.", envvar=environment.REFLEX_BACKEND_PORT.name, ) @click.option( "--backend-host", help="Specify the backend host.", ) @click.option( "--single-port", is_flag=True, help="Run both frontend and backend on the same port.", default=False, ) def run( env: LITERAL_ENV, frontend_only: bool, backend_only: bool, frontend_port: int | None, backend_port: int | None, backend_host: str | None, single_port: bool, ): """Run the app in the current directory.""" from reflex.utils import prerequisites if frontend_only and backend_only: console.error("Cannot use both --frontend-only and --backend-only options.") raise SystemExit(1) if single_port: if env != constants.Env.PROD: console.error("--single-port can only be used with --env=PROD.") raise SystemExit(1) if frontend_only or backend_only: console.error( "Cannot use --single-port with --frontend-only or --backend-only." ) raise SystemExit(1) if frontend_port and backend_port and frontend_port != backend_port: console.error( "Cannot specify different ports for frontend and backend when using --single-port." ) raise SystemExit(1) config = get_config() frontend_port = frontend_port or config.frontend_port backend_port = backend_port or config.backend_port backend_host = backend_host or config.backend_host environment.REFLEX_COMPILE_CONTEXT.set(constants.CompileContext.RUN) environment.REFLEX_BACKEND_ONLY.set(backend_only) environment.REFLEX_FRONTEND_ONLY.set(frontend_only) running_mode = prerequisites.check_running_mode(frontend_only, backend_only) _run( env=constants.Env.DEV if env == constants.Env.DEV else constants.Env.PROD, running_mode=running_mode, frontend_port=frontend_port, backend_port=backend_port, backend_host=backend_host, ) @cli.command() @loglevel_option @click.option( "--dry", is_flag=True, default=False, help="Run the command without making any changes.", ) @click.option( "--rich/--no-rich", default=True, is_flag=True, help="Whether to use rich progress bars.", ) def compile(dry: bool, rich: bool): """Compile the app in the current directory.""" import time from reflex.utils import prerequisites # Check the app. if prerequisites.needs_reinit(): _init(name=get_config().app_name) get_config(reload=True) starting_time = time.monotonic() prerequisites.get_compiled_app(dry_run=dry, use_rich=rich) elapsed_time = time.monotonic() - starting_time console.success(f"App compiled successfully in {elapsed_time:.3f} seconds.") @cli.command() @loglevel_option @click.option( "--zip/--no-zip", default=True, is_flag=True, help="Whether to zip the backend and frontend exports.", ) @click.option( "--frontend-only", is_flag=True, show_default=False, envvar=environment.REFLEX_FRONTEND_ONLY.name, help="Export only frontend.", ) @click.option( "--backend-only", is_flag=True, show_default=False, envvar=environment.REFLEX_BACKEND_ONLY.name, help="Export only backend.", ) @click.option( "--zip-dest-dir", default=str(Path.cwd()), help="The directory to export the zip files to.", show_default=False, ) @click.option( "--upload-db-file", is_flag=True, help="Whether to exclude sqlite db files when exporting backend.", hidden=True, ) @click.option( "--env", type=click.Choice([e.value for e in constants.Env], case_sensitive=False), default=constants.Env.PROD.value, help="The environment to export the app in.", ) @click.option( "--exclude-from-backend", "backend_excluded_dirs", multiple=True, type=click.Path(exists=True, path_type=Path, resolve_path=True), help="Files or directories to exclude from the backend zip. Can be used multiple times.", ) @click.option( "--server-side-rendering/--no-server-side-rendering", "--ssr/--no-ssr", "ssr", default=True, is_flag=True, help="Whether to enable server side rendering for the frontend.", ) def export( zip: bool, frontend_only: bool, backend_only: bool, zip_dest_dir: str, upload_db_file: bool, env: LITERAL_ENV, backend_excluded_dirs: tuple[Path, ...] = (), ssr: bool = True, ): """Export the app to a zip file.""" from reflex.utils import export as export_utils from reflex.utils import prerequisites if not environment.REFLEX_SSR.is_set(): environment.REFLEX_SSR.set(ssr) elif environment.REFLEX_SSR.get() != ssr: ssr = environment.REFLEX_SSR.get() environment.REFLEX_COMPILE_CONTEXT.set(constants.CompileContext.EXPORT) running_mode = prerequisites.check_running_mode(frontend_only, backend_only) config = get_config() prerequisites.assert_in_reflex_dir() if running_mode.has_frontend() and prerequisites.needs_reinit(): _init(name=config.app_name) export_utils.export( zipping=zip, frontend=running_mode.has_frontend(), backend=running_mode.has_backend(), zip_dest_dir=zip_dest_dir, upload_db_file=upload_db_file, env=constants.Env.DEV if env == constants.Env.DEV else constants.Env.PROD, loglevel=config.loglevel.subprocess_level(), backend_excluded_dirs=backend_excluded_dirs, prerender_routes=ssr, ) @cli.command() @loglevel_option def login(): """Authenticate with experimental Reflex hosting service.""" from reflex_cli.v2 import cli as hosting_cli from reflex_cli.v2.deployments import check_version check_version() validated_info = hosting_cli.login() if validated_info is not None: _skip_compile() # Allow running outside of an app dir from reflex.utils import telemetry telemetry.send("login", user_uuid=validated_info.get("user_id")) @cli.command() @loglevel_option def logout(): """Log out of access to Reflex hosting service.""" from reflex_cli.v2.cli import logout from reflex_cli.v2.deployments import check_version check_version() logout(_convert_reflex_loglevel_to_reflex_cli_loglevel(get_config().loglevel)) @click.group def db_cli(): """Subcommands for managing the database schema.""" @click.group def script_cli(): """Subcommands for running helper scripts.""" def _skip_compile(): """Skip the compile step.""" environment.REFLEX_SKIP_COMPILE.set(True) @db_cli.command(name="init") def db_init(): """Create database schema and migration configuration.""" from reflex import model from reflex.utils import prerequisites config = get_config() # Check the database url. if config.db_url is None: console.error("db_url is not configured, cannot initialize.") return # Check the alembic config. if environment.ALEMBIC_CONFIG.get().exists(): console.error( "Database is already initialized. Use " "[bold]reflex db makemigrations[/bold] to create schema change " "scripts and [bold]reflex db migrate[/bold] to apply migrations " "to a new or existing database.", ) return # Initialize the database. _skip_compile() prerequisites.get_compiled_app() model.alembic_init() model.migrate(autogenerate=True) @db_cli.command() def migrate(): """Create or update database schema from migration scripts.""" from reflex import model from reflex.utils import prerequisites prerequisites.get_app() if not prerequisites.check_db_initialized(): return model.migrate() prerequisites.check_schema_up_to_date() @db_cli.command() def status(): """Check the status of the database schema.""" from reflex.model import format_revision, get_migration_history from reflex.utils import prerequisites prerequisites.get_app() if not prerequisites.check_db_initialized(): console.info( "Database is not initialized. Run [bold]reflex db init[/bold] to initialize." ) return # Run alembic check command and display output import reflex_base.config config = reflex_base.config.get_config() console.print(f"[bold]\\[{config.db_url}][/bold]") # Get migration history using Model method current_rev, revisions = get_migration_history() if current_rev is None and not revisions: return current_reached_ref = [current_rev is None] # Show migration history in chronological order console.print("") for rev in revisions: # Format and print the revision console.print(format_revision(rev, current_rev, current_reached_ref)) @db_cli.command() @click.option( "--message", help="Human readable identifier for the generated revision.", ) def makemigrations(message: str | None): """Create autogenerated alembic migration scripts.""" from alembic.util.exc import CommandError from reflex import model from reflex.utils import prerequisites # TODO see if we can use `get_app()` instead (no compile). Would _skip_compile still be needed then? _skip_compile() prerequisites.get_compiled_app() if not prerequisites.check_db_initialized(): return with model.get_engine().connect() as connection: try: model.alembic_autogenerate(connection=connection, message=message) except CommandError as command_error: if "Target database is not up to date." not in str(command_error): raise console.error( f"{command_error} Run [bold]reflex db migrate[/bold] to update database." ) @cli.command() @loglevel_option @click.option( "--app-name", help="The name of the app to deploy.", ) @click.option( "--app-id", help="The ID of the app to deploy.", ) @click.option( "-r", "--region", multiple=True, help="The regions to deploy to. `reflex cloud regions` For multiple envs, repeat this option, e.g. --region sjc --region iad", ) @click.option( "--env", multiple=True, help="The environment variables to set: =. For multiple envs, repeat this option, e.g. --env k1=v2 --env k2=v2.", ) @click.option( "--vmtype", help="Vm type id. Run `reflex cloud vmtypes` to get options.", ) @click.option( "--hostname", help="The hostname of the frontend.", ) @click.option( "--interactive/--no-interactive", is_flag=True, default=True, help="Whether to list configuration options and ask for confirmation.", ) @click.option( "--envfile", help="The path to an env file to use. Will override any envs set manually.", ) @click.option( "--project", help="project id to deploy to", ) @click.option( "--project-name", help="The name of the project to deploy to.", ) @click.option( "--token", help="token to use for auth", ) @click.option( "--config-path", "--config", help="path to the config file", ) @click.option( "--exclude-from-backend", "backend_excluded_dirs", multiple=True, type=click.Path(exists=True, path_type=Path, resolve_path=True), help="Files or directories to exclude from the backend zip. Can be used multiple times.", ) @click.option( "--server-side-rendering/--no-server-side-rendering", "--ssr/--no-ssr", "ssr", default=True, is_flag=True, help="Whether to enable server side rendering for the frontend.", ) def deploy( app_name: str | None, app_id: str | None, region: tuple[str, ...], env: tuple[str], vmtype: str | None, hostname: str | None, interactive: bool, envfile: str | None, project: str | None, project_name: str | None, token: str | None, config_path: str | None, backend_excluded_dirs: tuple[Path, ...] = (), ssr: bool = True, ): """Deploy the app to the Reflex hosting service.""" from reflex_cli.utils import dependency from reflex_cli.v2 import cli as hosting_cli from reflex_cli.v2.deployments import check_version from reflex.utils import export as export_utils from reflex.utils import prerequisites config = get_config() app_name = app_name or config.app_name check_version() environment.REFLEX_COMPILE_CONTEXT.set(constants.CompileContext.DEPLOY) if not environment.REFLEX_SSR.is_set(): environment.REFLEX_SSR.set(ssr) elif environment.REFLEX_SSR.get() != ssr: ssr = environment.REFLEX_SSR.get() # Only check requirements if interactive. # There is user interaction for requirements update. if interactive: dependency.check_requirements() prerequisites.assert_in_reflex_dir() # Check if we are set up. if prerequisites.needs_reinit(): _init(name=config.app_name) prerequisites.check_latest_package_version(constants.ReflexHostingCLI.MODULE_NAME) hosting_cli.deploy( app_name=app_name, app_id=app_id, export_fn=( lambda zip_dest_dir, api_url, deploy_url, frontend, backend, upload_db, zipping: ( export_utils.export( zip_dest_dir=zip_dest_dir, api_url=api_url, deploy_url=deploy_url, frontend=frontend, backend=backend, zipping=zipping, loglevel=config.loglevel.subprocess_level(), upload_db_file=upload_db, backend_excluded_dirs=backend_excluded_dirs, prerender_routes=ssr, ) ) ), regions=list(region), envs=list(env), vmtype=vmtype, envfile=envfile, hostname=hostname, interactive=interactive, loglevel=_convert_reflex_loglevel_to_reflex_cli_loglevel(config.loglevel), token=token, project=project, project_name=project_name, **({"config_path": config_path} if config_path is not None else {}), ) @cli.command() @loglevel_option @click.argument("new_name") def rename(new_name: str): """Rename the app in the current directory.""" from reflex.utils import prerequisites from reflex.utils.rename import rename_app prerequisites.validate_app_name(new_name) rename_app(new_name, get_config().loglevel) def _convert_reflex_loglevel_to_reflex_cli_loglevel( loglevel: constants.LogLevel, ) -> HostingLogLevel: """Convert a Reflex log level to a Reflex CLI log level. Args: loglevel: The Reflex log level to convert. Returns: The converted Reflex CLI log level. """ from reflex_cli.constants.base import LogLevel as HostingLogLevel if loglevel == constants.LogLevel.DEBUG: return HostingLogLevel.DEBUG if loglevel == constants.LogLevel.INFO: return HostingLogLevel.INFO if loglevel == constants.LogLevel.WARNING: return HostingLogLevel.WARNING if loglevel == constants.LogLevel.ERROR: return HostingLogLevel.ERROR if loglevel == constants.LogLevel.CRITICAL: return HostingLogLevel.CRITICAL return HostingLogLevel.INFO if find_spec("typer") and find_spec("typer.main"): import typer # pyright: ignore[reportMissingImports] if isinstance(hosting_cli, typer.Typer): hosting_cli_command = typer.main.get_command(hosting_cli) else: hosting_cli_command = hosting_cli else: hosting_cli_command = hosting_cli cli.add_command(hosting_cli_command, name="cloud") cli.add_command(db_cli, name="db") cli.add_command(script_cli, name="script") cli.add_command(custom_components_cli, name="component") if __name__ == "__main__": cli()