瀏覽代碼

click over typer (#5154)

* click over typer

* these are flags

* i missed a few

* fix a few more

* zip

* factor out loglevel debug

* case insensitive for everyone

* do things a bit smarter

* fix pyright

* fix app harness

* use export directly

* uv lock without trailing slash

* last fixes

* unpin weird stuff

* no need to export typer

* that guy too

* restore uv lokc

* uv lock

* precommit silly

* lock

* why did i do that

* factor things out
Khaleel Al-Adhami 3 周之前
父節點
當前提交
27bb987f78

+ 2 - 2
pyproject.toml

@@ -32,10 +32,10 @@ dependencies = [
   "python-socketio >=5.12.0,<6.0",
   "python-socketio >=5.12.0,<6.0",
   "python-multipart >=0.0.20,<1.0",
   "python-multipart >=0.0.20,<1.0",
   "redis >=5.2.1,<6.0",
   "redis >=5.2.1,<6.0",
-  "reflex-hosting-cli >=0.1.38",
+  "reflex-hosting-cli >=0.1.43",
   "rich >=13,<15",
   "rich >=13,<15",
   "sqlmodel >=0.0.24,<0.1",
   "sqlmodel >=0.0.24,<0.1",
-  "typer >=0.15.2,<1.0",
+  "click >=8",
   "typing_extensions >=4.13.0",
   "typing_extensions >=4.13.0",
   "wrapt >=1.17.0,<2.0",
   "wrapt >=1.17.0,<2.0",
 ]
 ]

+ 1 - 1
reflex/config.py

@@ -903,7 +903,7 @@ class Config(Base):
         # Set the log level for this process
         # Set the log level for this process
         env_loglevel = os.environ.get("LOGLEVEL")
         env_loglevel = os.environ.get("LOGLEVEL")
         if env_loglevel is not None:
         if env_loglevel is not None:
-            env_loglevel = LogLevel(env_loglevel)
+            env_loglevel = LogLevel(env_loglevel.lower())
         if env_loglevel or self.loglevel != LogLevel.DEFAULT:
         if env_loglevel or self.loglevel != LogLevel.DEFAULT:
             console.set_log_level(env_loglevel or self.loglevel)
             console.set_log_level(env_loglevel or self.loglevel)
 
 

+ 21 - 0
reflex/constants/base.py

@@ -7,6 +7,7 @@ from enum import Enum
 from importlib import metadata
 from importlib import metadata
 from pathlib import Path
 from pathlib import Path
 from types import SimpleNamespace
 from types import SimpleNamespace
+from typing import Literal
 
 
 from platformdirs import PlatformDirs
 from platformdirs import PlatformDirs
 
 
@@ -219,6 +220,9 @@ class ColorMode(SimpleNamespace):
     SET = "setColorMode"
     SET = "setColorMode"
 
 
 
 
+LITERAL_ENV = Literal["dev", "prod"]
+
+
 # Env modes
 # Env modes
 class Env(str, Enum):
 class Env(str, Enum):
     """The environment modes."""
     """The environment modes."""
@@ -238,6 +242,23 @@ class LogLevel(str, Enum):
     ERROR = "error"
     ERROR = "error"
     CRITICAL = "critical"
     CRITICAL = "critical"
 
 
+    @classmethod
+    def from_string(cls, level: str | None) -> LogLevel | None:
+        """Convert a string to a log level.
+
+        Args:
+            level: The log level as a string.
+
+        Returns:
+            The log level.
+        """
+        if not level:
+            return None
+        try:
+            return LogLevel[level.upper()]
+        except KeyError:
+            return None
+
     def __le__(self, other: LogLevel) -> bool:
     def __le__(self, other: LogLevel) -> bool:
         """Compare log levels.
         """Compare log levels.
 
 

+ 67 - 64
reflex/custom_components/custom_components.py

@@ -9,17 +9,46 @@ import sys
 from collections import namedtuple
 from collections import namedtuple
 from contextlib import contextmanager
 from contextlib import contextmanager
 from pathlib import Path
 from pathlib import Path
+from typing import Any
 
 
+import click
 import httpx
 import httpx
-import typer
 
 
 from reflex import constants
 from reflex import constants
-from reflex.config import get_config
 from reflex.constants import CustomComponents
 from reflex.constants import CustomComponents
 from reflex.utils import console
 from reflex.utils import console
 
 
-custom_components_cli = typer.Typer()
 
 
+def set_loglevel(ctx: Any, self: Any, 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
+def custom_components_cli():
+    """CLI for creating custom components."""
+    pass
+
+
+loglevel_option = click.option(
+    "--loglevel",
+    type=click.Choice(
+        [loglevel.value for loglevel in constants.LogLevel],
+        case_sensitive=False,
+    ),
+    callback=set_loglevel,
+    is_eager=True,
+    expose_value=False,
+    help="The log level to use.",
+)
 
 
 POST_CUSTOM_COMPONENTS_GALLERY_TIMEOUT = 15
 POST_CUSTOM_COMPONENTS_GALLERY_TIMEOUT = 15
 
 
@@ -163,13 +192,13 @@ def _get_default_library_name_parts() -> list[str]:
             console.error(
             console.error(
                 f"Based on current directory name {current_dir_name}, the library name is {constants.Reflex.MODULE_NAME}. This package already exists. Please use --library-name to specify a different name."
                 f"Based on current directory name {current_dir_name}, the library name is {constants.Reflex.MODULE_NAME}. This package already exists. Please use --library-name to specify a different name."
             )
             )
-            raise typer.Exit(code=1)
+            raise click.exceptions.Exit(code=1)
     if not parts:
     if not parts:
         # The folder likely has a name not suitable for python paths.
         # The folder likely has a name not suitable for python paths.
         console.error(
         console.error(
             f"Could not find a valid library name based on the current directory: got {current_dir_name}."
             f"Could not find a valid library name based on the current directory: got {current_dir_name}."
         )
         )
-        raise typer.Exit(code=1)
+        raise click.exceptions.Exit(code=1)
     return parts
     return parts
 
 
 
 
@@ -205,7 +234,7 @@ def _validate_library_name(library_name: str | None) -> NameVariants:
         console.error(
         console.error(
             f"Please use only alphanumeric characters or dashes: got {library_name}"
             f"Please use only alphanumeric characters or dashes: got {library_name}"
         )
         )
-        raise typer.Exit(code=1)
+        raise click.exceptions.Exit(code=1)
 
 
     # If not specified, use the current directory name to form the module name.
     # If not specified, use the current directory name to form the module name.
     name_parts = (
     name_parts = (
@@ -277,36 +306,35 @@ def _populate_custom_component_project(name_variants: NameVariants):
 
 
 
 
 @custom_components_cli.command(name="init")
 @custom_components_cli.command(name="init")
+@click.option(
+    "--library-name",
+    default=None,
+    help="The name of your library. On PyPI, package will be published as `reflex-{library-name}`.",
+)
+@click.option(
+    "--install/--no-install",
+    default=True,
+    help="Whether to install package from this local custom component in editable mode.",
+)
+@loglevel_option
 def init(
 def init(
-    library_name: str | None = typer.Option(
-        None,
-        help="The name of your library. On PyPI, package will be published as `reflex-{library-name}`.",
-    ),
-    install: bool = typer.Option(
-        True,
-        help="Whether to install package from this local custom component in editable mode.",
-    ),
-    loglevel: constants.LogLevel | None = typer.Option(
-        None, help="The log level to use."
-    ),
+    library_name: str | None,
+    install: bool,
 ):
 ):
     """Initialize a custom component.
     """Initialize a custom component.
 
 
     Args:
     Args:
         library_name: The name of the library.
         library_name: The name of the library.
         install: Whether to install package from this local custom component in editable mode.
         install: Whether to install package from this local custom component in editable mode.
-        loglevel: The log level to use.
 
 
     Raises:
     Raises:
         Exit: If the pyproject.toml already exists.
         Exit: If the pyproject.toml already exists.
     """
     """
     from reflex.utils import exec, prerequisites
     from reflex.utils import exec, prerequisites
 
 
-    console.set_log_level(loglevel or get_config().loglevel)
-
     if CustomComponents.PYPROJECT_TOML.exists():
     if CustomComponents.PYPROJECT_TOML.exists():
         console.error(f"A {CustomComponents.PYPROJECT_TOML} already exists. Aborting.")
         console.error(f"A {CustomComponents.PYPROJECT_TOML} already exists. Aborting.")
-        typer.Exit(code=1)
+        click.exceptions.Exit(code=1)
 
 
     # Show system info.
     # Show system info.
     exec.output_system_info()
     exec.output_system_info()
@@ -331,7 +359,7 @@ def init(
         if _pip_install_on_demand(package_name=".", install_args=["-e"]):
         if _pip_install_on_demand(package_name=".", install_args=["-e"]):
             console.info(f"Package {package_name} installed!")
             console.info(f"Package {package_name} installed!")
         else:
         else:
-            raise typer.Exit(code=1)
+            raise click.exceptions.Exit(code=1)
 
 
     console.print("[bold]Custom component initialized successfully!")
     console.print("[bold]Custom component initialized successfully!")
     console.rule("[bold]Project Summary")
     console.rule("[bold]Project Summary")
@@ -424,21 +452,13 @@ def _run_build():
     if _run_commands_in_subprocess(cmds):
     if _run_commands_in_subprocess(cmds):
         console.info("Custom component built successfully!")
         console.info("Custom component built successfully!")
     else:
     else:
-        raise typer.Exit(code=1)
+        raise click.exceptions.Exit(code=1)
 
 
 
 
 @custom_components_cli.command(name="build")
 @custom_components_cli.command(name="build")
-def build(
-    loglevel: constants.LogLevel | None = typer.Option(
-        None, help="The log level to use."
-    ),
-):
-    """Build a custom component. Must be run from the project root directory where the pyproject.toml is.
-
-    Args:
-        loglevel: The log level to use.
-    """
-    console.set_log_level(loglevel or get_config().loglevel)
+@loglevel_option
+def build():
+    """Build a custom component. Must be run from the project root directory where the pyproject.toml is."""
     _run_build()
     _run_build()
 
 
 
 
@@ -453,7 +473,7 @@ def publish():
         "The publish command is deprecated. You can use `reflex component build` followed by `twine upload` or a similar publishing command to publish your custom component."
         "The publish command is deprecated. You can use `reflex component build` followed by `twine upload` or a similar publishing command to publish your custom component."
         "\nIf you want to share your custom component with the Reflex community, please use `reflex component share`."
         "\nIf you want to share your custom component with the Reflex community, please use `reflex component share`."
     )
     )
-    raise typer.Exit(code=1)
+    raise click.exceptions.Exit(code=1)
 
 
 
 
 def _collect_details_for_gallery():
 def _collect_details_for_gallery():
@@ -472,7 +492,7 @@ def _collect_details_for_gallery():
         console.error(
         console.error(
             "Unable to authenticate with Reflex backend services. Make sure you are logged in."
             "Unable to authenticate with Reflex backend services. Make sure you are logged in."
         )
         )
-        raise typer.Exit(code=1)
+        raise click.exceptions.Exit(code=1)
 
 
     console.rule("[bold]Custom Component Information")
     console.rule("[bold]Custom Component Information")
     params = {}
     params = {}
@@ -502,11 +522,11 @@ def _collect_details_for_gallery():
             console.error(
             console.error(
                 f"{package_name} is owned by another user. Unable to update the information for it."
                 f"{package_name} is owned by another user. Unable to update the information for it."
             )
             )
-            raise typer.Exit(code=1)
+            raise click.exceptions.Exit(code=1)
         response.raise_for_status()
         response.raise_for_status()
     except httpx.HTTPError as he:
     except httpx.HTTPError as he:
         console.error(f"Unable to complete request due to {he}.")
         console.error(f"Unable to complete request due to {he}.")
-        raise typer.Exit(code=1) from he
+        raise click.exceptions.Exit(code=1) from he
 
 
     files = []
     files = []
     if (image_file_and_extension := _get_file_from_prompt_in_loop()) is not None:
     if (image_file_and_extension := _get_file_from_prompt_in_loop()) is not None:
@@ -541,7 +561,7 @@ def _collect_details_for_gallery():
 
 
     except httpx.HTTPError as he:
     except httpx.HTTPError as he:
         console.error(f"Unable to complete request due to {he}.")
         console.error(f"Unable to complete request due to {he}.")
-        raise typer.Exit(code=1) from he
+        raise click.exceptions.Exit(code=1) from he
 
 
     console.info("Custom component information successfully shared!")
     console.info("Custom component information successfully shared!")
 
 
@@ -577,7 +597,7 @@ def _get_file_from_prompt_in_loop() -> tuple[bytes, str] | None:
             image_file = image_file_path.read_bytes()
             image_file = image_file_path.read_bytes()
         except OSError as ose:
         except OSError as ose:
             console.error(f"Unable to read the {file_extension} file due to {ose}")
             console.error(f"Unable to read the {file_extension} file due to {ose}")
-            raise typer.Exit(code=1) from ose
+            raise click.exceptions.Exit(code=1) from ose
         else:
         else:
             return image_file, file_extension
             return image_file, file_extension
 
 
@@ -586,38 +606,21 @@ def _get_file_from_prompt_in_loop() -> tuple[bytes, str] | None:
 
 
 
 
 @custom_components_cli.command(name="share")
 @custom_components_cli.command(name="share")
-def share_more_detail(
-    loglevel: constants.LogLevel | None = typer.Option(
-        None, help="The log level to use."
-    ),
-):
-    """Collect more details on the published package for gallery.
-
-    Args:
-        loglevel: The log level to use.
-    """
-    console.set_log_level(loglevel or get_config().loglevel)
-
+@loglevel_option
+def share_more_detail():
+    """Collect more details on the published package for gallery."""
     _collect_details_for_gallery()
     _collect_details_for_gallery()
 
 
 
 
-@custom_components_cli.command()
-def install(
-    loglevel: constants.LogLevel | None = typer.Option(
-        None, help="The log level to use."
-    ),
-):
+@custom_components_cli.command(name="install")
+@loglevel_option
+def install():
     """Install package from this local custom component in editable mode.
     """Install package from this local custom component in editable mode.
 
 
-    Args:
-        loglevel: The log level to use.
-
     Raises:
     Raises:
         Exit: If unable to install the current directory in editable mode.
         Exit: If unable to install the current directory in editable mode.
     """
     """
-    console.set_log_level(loglevel or get_config().loglevel)
-
     if _pip_install_on_demand(package_name=".", install_args=["-e"]):
     if _pip_install_on_demand(package_name=".", install_args=["-e"]):
         console.info("Package installed successfully!")
         console.info("Package installed successfully!")
     else:
     else:
-        raise typer.Exit(code=1)
+        raise click.exceptions.Exit(code=1)

+ 276 - 265
reflex/reflex.py

@@ -3,74 +3,63 @@
 from __future__ import annotations
 from __future__ import annotations
 
 
 import atexit
 import atexit
+from importlib.util import find_spec
 from pathlib import Path
 from pathlib import Path
 from typing import TYPE_CHECKING
 from typing import TYPE_CHECKING
 
 
-import typer
-import typer.core
+import click
 from reflex_cli.v2.deployments import hosting_cli
 from reflex_cli.v2.deployments import hosting_cli
 
 
 from reflex import constants
 from reflex import constants
 from reflex.config import environment, get_config
 from reflex.config import environment, get_config
+from reflex.constants.base import LITERAL_ENV
 from reflex.custom_components.custom_components import custom_components_cli
 from reflex.custom_components.custom_components import custom_components_cli
 from reflex.state import reset_disk_state_manager
 from reflex.state import reset_disk_state_manager
 from reflex.utils import console, redir, telemetry
 from reflex.utils import console, redir, telemetry
 from reflex.utils.exec import should_use_granian
 from reflex.utils.exec import should_use_granian
 
 
-# Disable typer+rich integration for help panels
-typer.core.rich = None  # pyright: ignore [reportPrivateImportUsage]
 
 
-# Create the app.
-cli = typer.Typer(add_completion=False, pretty_exceptions_enable=False)
-
-
-def version(value: bool):
-    """Get the Reflex version.
+def set_loglevel(ctx: click.Context, self: click.Parameter, value: str | None):
+    """Set the log level.
 
 
     Args:
     Args:
-        value: Whether the version flag was passed.
-
-    Raises:
-        typer.Exit: If the version flag was passed.
+        ctx: The click context.
+        self: The click command.
+        value: The log level to set.
     """
     """
-    if value:
-        console.print(constants.Reflex.VERSION)
-        raise typer.Exit()
-
-
-@cli.callback()
-def main(
-    version: bool = typer.Option(
-        None,
-        "-v",
-        "--version",
-        callback=version,
-        help="Get the Reflex version.",
-        is_eager=True,
-    ),
-):
+    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."""
     """Reflex CLI to create, run, and deploy apps."""
     pass
     pass
 
 
 
 
+loglevel_option = click.option(
+    "--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(
 def _init(
     name: str,
     name: str,
     template: str | None = None,
     template: str | None = None,
-    loglevel: constants.LogLevel | None = None,
     ai: bool = False,
     ai: bool = False,
 ):
 ):
     """Initialize a new Reflex app in the given directory."""
     """Initialize a new Reflex app in the given directory."""
     from reflex.utils import exec, prerequisites
     from reflex.utils import exec, prerequisites
 
 
-    if loglevel is not None:
-        console.set_log_level(loglevel)
-
-    config = get_config()
-
-    # Set the log level.
-    loglevel = loglevel or config.loglevel
-    console.set_log_level(loglevel)
-
     # Show system info
     # Show system info
     exec.output_system_info()
     exec.output_system_info()
 
 
@@ -112,24 +101,28 @@ def _init(
 
 
 
 
 @cli.command()
 @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(
 def init(
-    name: str = typer.Option(
-        None, metavar="APP_NAME", help="The name of the app to initialize."
-    ),
-    template: str = typer.Option(
-        None,
-        help="The template to initialize the app with.",
-    ),
-    loglevel: constants.LogLevel | None = typer.Option(
-        None, help="The log level to use."
-    ),
-    ai: bool = typer.Option(
-        False,
-        help="Use AI to create the initial template. Cannot be used with existing app or `--template` option.",
-    ),
+    name: str,
+    template: str | None,
+    ai: bool,
 ):
 ):
     """Initialize a new Reflex app in the current directory."""
     """Initialize a new Reflex app in the current directory."""
-    _init(name, template, loglevel, ai)
+    _init(name, template, ai)
 
 
 
 
 def _run(
 def _run(
@@ -139,22 +132,14 @@ def _run(
     frontend_port: int | None = None,
     frontend_port: int | None = None,
     backend_port: int | None = None,
     backend_port: int | None = None,
     backend_host: str | None = None,
     backend_host: str | None = None,
-    loglevel: constants.LogLevel | None = None,
 ):
 ):
     """Run the app in the given directory."""
     """Run the app in the given directory."""
     from reflex.utils import build, exec, prerequisites, processes
     from reflex.utils import build, exec, prerequisites, processes
 
 
-    if loglevel is not None:
-        console.set_log_level(loglevel)
-
     config = get_config()
     config = get_config()
 
 
-    loglevel = loglevel or config.loglevel
     backend_host = backend_host or config.backend_host
     backend_host = backend_host or config.backend_host
 
 
-    # Set the log level.
-    console.set_log_level(loglevel)
-
     # Set env mode in the environment
     # Set env mode in the environment
     environment.REFLEX_ENV_MODE.set(env)
     environment.REFLEX_ENV_MODE.set(env)
 
 
@@ -168,7 +153,7 @@ def _run(
 
 
     # Check that the app is initialized.
     # Check that the app is initialized.
     if prerequisites.needs_reinit(frontend=frontend):
     if prerequisites.needs_reinit(frontend=frontend):
-        _init(name=config.app_name, loglevel=loglevel)
+        _init(name=config.app_name)
 
 
     # Delete the states folder if it exists.
     # Delete the states folder if it exists.
     reset_disk_state_manager()
     reset_disk_state_manager()
@@ -228,7 +213,7 @@ def _run(
     else:
     else:
         validation_result = app_task(*args)
         validation_result = app_task(*args)
     if not validation_result:
     if not validation_result:
-        raise typer.Exit(1)
+        raise click.exceptions.Exit(1)
 
 
     # Warn if schema is not up to date.
     # Warn if schema is not up to date.
     prerequisites.check_schema_up_to_date()
     prerequisites.check_schema_up_to_date()
@@ -248,7 +233,7 @@ def _run(
             exec.run_backend_prod,
             exec.run_backend_prod,
         )
         )
     if not setup_frontend or not frontend_cmd or not backend_cmd:
     if not setup_frontend or not frontend_cmd or not backend_cmd:
-        raise ValueError("Invalid env")
+        raise ValueError(f"Invalid env: {env}. Must be DEV or PROD.")
 
 
     # Post a telemetry event.
     # Post a telemetry event.
     telemetry.send(f"run-{env.value}")
     telemetry.send(f"run-{env.value}")
@@ -271,7 +256,7 @@ def _run(
                 backend_cmd,
                 backend_cmd,
                 backend_host,
                 backend_host,
                 backend_port,
                 backend_port,
-                loglevel.subprocess_level(),
+                config.loglevel.subprocess_level(),
                 frontend,
                 frontend,
             )
             )
         )
         )
@@ -281,7 +266,10 @@ def _run(
         # In dev mode, run the backend on the main thread.
         # In dev mode, run the backend on the main thread.
         if backend and backend_port and env == constants.Env.DEV:
         if backend and backend_port and env == constants.Env.DEV:
             backend_cmd(
             backend_cmd(
-                backend_host, int(backend_port), loglevel.subprocess_level(), frontend
+                backend_host,
+                int(backend_port),
+                config.loglevel.subprocess_level(),
+                frontend,
             )
             )
             # The windows uvicorn bug workaround
             # The windows uvicorn bug workaround
             # https://github.com/reflex-dev/reflex/issues/2335
             # https://github.com/reflex-dev/reflex/issues/2335
@@ -291,94 +279,122 @@ def _run(
 
 
 
 
 @cli.command()
 @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.",
+)
 def run(
 def run(
-    env: constants.Env = typer.Option(
-        constants.Env.DEV, help="The environment to run the app in."
-    ),
-    frontend: bool = typer.Option(
-        False,
-        "--frontend-only",
-        help="Execute only frontend.",
-        envvar=environment.REFLEX_FRONTEND_ONLY.name,
-    ),
-    backend: bool = typer.Option(
-        False,
-        "--backend-only",
-        help="Execute only backend.",
-        envvar=environment.REFLEX_BACKEND_ONLY.name,
-    ),
-    frontend_port: int | None = typer.Option(
-        None,
-        help="Specify a different frontend port.",
-        envvar=environment.REFLEX_FRONTEND_PORT.name,
-    ),
-    backend_port: int | None = typer.Option(
-        None,
-        help="Specify a different backend port.",
-        envvar=environment.REFLEX_BACKEND_PORT.name,
-    ),
-    backend_host: str | None = typer.Option(None, help="Specify the backend host."),
-    loglevel: constants.LogLevel | None = typer.Option(
-        None, help="The log level to use."
-    ),
+    env: LITERAL_ENV,
+    frontend_only: bool,
+    backend_only: bool,
+    frontend_port: int | None,
+    backend_port: int | None,
+    backend_host: str | None,
 ):
 ):
     """Run the app in the current directory."""
     """Run the app in the current directory."""
-    if frontend and backend:
+    if frontend_only and backend_only:
         console.error("Cannot use both --frontend-only and --backend-only options.")
         console.error("Cannot use both --frontend-only and --backend-only options.")
-        raise typer.Exit(1)
-
-    if loglevel is not None:
-        console.set_log_level(loglevel)
+        raise click.exceptions.Exit(1)
 
 
     config = get_config()
     config = get_config()
 
 
     frontend_port = frontend_port or config.frontend_port
     frontend_port = frontend_port or config.frontend_port
     backend_port = backend_port or config.backend_port
     backend_port = backend_port or config.backend_port
     backend_host = backend_host or config.backend_host
     backend_host = backend_host or config.backend_host
-    loglevel = loglevel or config.loglevel
 
 
     environment.REFLEX_COMPILE_CONTEXT.set(constants.CompileContext.RUN)
     environment.REFLEX_COMPILE_CONTEXT.set(constants.CompileContext.RUN)
-    environment.REFLEX_BACKEND_ONLY.set(backend)
-    environment.REFLEX_FRONTEND_ONLY.set(frontend)
-
-    _run(env, frontend, backend, frontend_port, backend_port, backend_host, loglevel)
+    environment.REFLEX_BACKEND_ONLY.set(backend_only)
+    environment.REFLEX_FRONTEND_ONLY.set(frontend_only)
+
+    _run(
+        constants.Env.DEV if env == constants.Env.DEV else constants.Env.PROD,
+        frontend_only,
+        backend_only,
+        frontend_port,
+        backend_port,
+        backend_host,
+    )
 
 
 
 
 @cli.command()
 @cli.command()
+@loglevel_option
+@click.option(
+    "--zip/--no-zip",
+    default=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.",
+)
 def export(
 def export(
-    zipping: bool = typer.Option(
-        True, "--no-zip", help="Disable zip for backend and frontend exports."
-    ),
-    frontend: bool = typer.Option(
-        False,
-        "--frontend-only",
-        help="Export only frontend.",
-        show_default=False,
-        envvar=environment.REFLEX_FRONTEND_ONLY.name,
-    ),
-    backend: bool = typer.Option(
-        False,
-        "--backend-only",
-        help="Export only backend.",
-        show_default=False,
-        envvar=environment.REFLEX_BACKEND_ONLY.name,
-    ),
-    zip_dest_dir: str = typer.Option(
-        str(Path.cwd()),
-        help="The directory to export the zip files to.",
-        show_default=False,
-    ),
-    upload_db_file: bool = typer.Option(
-        False,
-        help="Whether to exclude sqlite db files when exporting backend.",
-        hidden=True,
-    ),
-    env: constants.Env = typer.Option(
-        constants.Env.PROD, help="The environment to export the app in."
-    ),
-    loglevel: constants.LogLevel | None = typer.Option(
-        None, help="The log level to use."
-    ),
+    zip: bool,
+    frontend_only: bool,
+    backend_only: bool,
+    zip_dest_dir: str,
+    upload_db_file: bool,
+    env: LITERAL_ENV,
 ):
 ):
     """Export the app to a zip file."""
     """Export the app to a zip file."""
     from reflex.utils import export as export_utils
     from reflex.utils import export as export_utils
@@ -386,35 +402,33 @@ def export(
 
 
     environment.REFLEX_COMPILE_CONTEXT.set(constants.CompileContext.EXPORT)
     environment.REFLEX_COMPILE_CONTEXT.set(constants.CompileContext.EXPORT)
 
 
-    frontend, backend = prerequisites.check_running_mode(frontend, backend)
+    frontend_only, backend_only = prerequisites.check_running_mode(
+        frontend_only, backend_only
+    )
 
 
-    loglevel = loglevel or get_config().loglevel
-    console.set_log_level(loglevel)
+    config = get_config()
 
 
-    if prerequisites.needs_reinit(frontend=frontend or not backend):
-        _init(name=get_config().app_name, loglevel=loglevel)
+    if prerequisites.needs_reinit(frontend=frontend_only or not backend_only):
+        _init(name=config.app_name)
 
 
     export_utils.export(
     export_utils.export(
-        zipping=zipping,
-        frontend=frontend,
-        backend=backend,
+        zipping=zip,
+        frontend=frontend_only,
+        backend=backend_only,
         zip_dest_dir=zip_dest_dir,
         zip_dest_dir=zip_dest_dir,
         upload_db_file=upload_db_file,
         upload_db_file=upload_db_file,
-        env=env,
-        loglevel=loglevel.subprocess_level(),
+        env=constants.Env.DEV if env == constants.Env.DEV else constants.Env.PROD,
+        loglevel=config.loglevel.subprocess_level(),
     )
     )
 
 
 
 
 @cli.command()
 @cli.command()
-def login(loglevel: constants.LogLevel | None = typer.Option(None)):
+@loglevel_option
+def login():
     """Authenticate with experimental Reflex hosting service."""
     """Authenticate with experimental Reflex hosting service."""
     from reflex_cli.v2 import cli as hosting_cli
     from reflex_cli.v2 import cli as hosting_cli
     from reflex_cli.v2.deployments import check_version
     from reflex_cli.v2.deployments import check_version
 
 
-    loglevel = loglevel or get_config().loglevel
-
-    console.set_log_level(loglevel)
-
     check_version()
     check_version()
 
 
     validated_info = hosting_cli.login()
     validated_info = hosting_cli.login()
@@ -424,24 +438,27 @@ def login(loglevel: constants.LogLevel | None = typer.Option(None)):
 
 
 
 
 @cli.command()
 @cli.command()
-def logout(
-    loglevel: constants.LogLevel | None = typer.Option(
-        None, help="The log level to use."
-    ),
-):
+@loglevel_option
+def logout():
     """Log out of access to Reflex hosting service."""
     """Log out of access to Reflex hosting service."""
     from reflex_cli.v2.cli import logout
     from reflex_cli.v2.cli import logout
     from reflex_cli.v2.deployments import check_version
     from reflex_cli.v2.deployments import check_version
 
 
     check_version()
     check_version()
 
 
-    loglevel = loglevel or get_config().loglevel
+    logout(_convert_reflex_loglevel_to_reflex_cli_loglevel(get_config().loglevel))
 
 
-    logout(_convert_reflex_loglevel_to_reflex_cli_loglevel(loglevel))
 
 
+@click.group
+def db_cli():
+    """Subcommands for managing the database schema."""
+    pass
 
 
-db_cli = typer.Typer()
-script_cli = typer.Typer()
+
+@click.group
+def script_cli():
+    """Subcommands for running helper scripts."""
+    pass
 
 
 
 
 def _skip_compile():
 def _skip_compile():
@@ -495,11 +512,11 @@ def migrate():
 
 
 
 
 @db_cli.command()
 @db_cli.command()
-def makemigrations(
-    message: str = typer.Option(
-        None, help="Human readable identifier for the generated revision."
-    ),
-):
+@click.option(
+    "--message",
+    help="Human readable identifier for the generated revision.",
+)
+def makemigrations(message: str | None):
     """Create autogenerated alembic migration scripts."""
     """Create autogenerated alembic migration scripts."""
     from alembic.util.exc import CommandError
     from alembic.util.exc import CommandError
 
 
@@ -523,70 +540,74 @@ def makemigrations(
 
 
 
 
 @cli.command()
 @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: <key>=<value>. 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",
+    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",
+)
 def deploy(
 def deploy(
-    app_name: str | None = typer.Option(
-        None,
-        "--app-name",
-        help="The name of the App to deploy under.",
-    ),
-    app_id: str = typer.Option(
-        None,
-        "--app-id",
-        help="The ID of the App to deploy over.",
-    ),
-    regions: list[str] = typer.Option(
-        [],
-        "-r",
-        "--region",
-        help="The regions to deploy to. `reflex cloud regions` For multiple envs, repeat this option, e.g. --region sjc --region iad",
-    ),
-    envs: list[str] = typer.Option(
-        [],
-        "--env",
-        help="The environment variables to set: <key>=<value>. For multiple envs, repeat this option, e.g. --env k1=v2 --env k2=v2.",
-    ),
-    vmtype: str | None = typer.Option(
-        None,
-        "--vmtype",
-        help="Vm type id. Run `reflex cloud vmtypes` to get options.",
-    ),
-    hostname: str | None = typer.Option(
-        None,
-        "--hostname",
-        help="The hostname of the frontend.",
-    ),
-    interactive: bool = typer.Option(
-        True,
-        help="Whether to list configuration options and ask for confirmation.",
-    ),
-    envfile: str | None = typer.Option(
-        None,
-        "--envfile",
-        help="The path to an env file to use. Will override any envs set manually.",
-    ),
-    loglevel: constants.LogLevel | None = typer.Option(
-        None, help="The log level to use."
-    ),
-    project: str | None = typer.Option(
-        None,
-        "--project",
-        help="project id to deploy to",
-    ),
-    project_name: str | None = typer.Option(
-        None,
-        "--project-name",
-        help="The name of the project to deploy to.",
-    ),
-    token: str | None = typer.Option(
-        None,
-        "--token",
-        help="token to use for auth",
-    ),
-    config_path: str | None = typer.Option(
-        None,
-        "--config",
-        help="path to the config file",
-    ),
+    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,
 ):
 ):
     """Deploy the app to the Reflex hosting service."""
     """Deploy the app to the Reflex hosting service."""
     from reflex_cli.utils import dependency
     from reflex_cli.utils import dependency
@@ -596,21 +617,14 @@ def deploy(
     from reflex.utils import export as export_utils
     from reflex.utils import export as export_utils
     from reflex.utils import prerequisites
     from reflex.utils import prerequisites
 
 
-    if loglevel is not None:
-        console.set_log_level(loglevel)
-
     config = get_config()
     config = get_config()
 
 
-    loglevel = loglevel or config.loglevel
     app_name = app_name or config.app_name
     app_name = app_name or config.app_name
 
 
     check_version()
     check_version()
 
 
     environment.REFLEX_COMPILE_CONTEXT.set(constants.CompileContext.DEPLOY)
     environment.REFLEX_COMPILE_CONTEXT.set(constants.CompileContext.DEPLOY)
 
 
-    # Set the log level.
-    console.set_log_level(loglevel)
-
     # Only check requirements if interactive.
     # Only check requirements if interactive.
     # There is user interaction for requirements update.
     # There is user interaction for requirements update.
     if interactive:
     if interactive:
@@ -618,7 +632,7 @@ def deploy(
 
 
     # Check if we are set up.
     # Check if we are set up.
     if prerequisites.needs_reinit(frontend=True):
     if prerequisites.needs_reinit(frontend=True):
-        _init(name=config.app_name, loglevel=loglevel)
+        _init(name=config.app_name)
     prerequisites.check_latest_package_version(constants.ReflexHostingCLI.MODULE_NAME)
     prerequisites.check_latest_package_version(constants.ReflexHostingCLI.MODULE_NAME)
 
 
     hosting_cli.deploy(
     hosting_cli.deploy(
@@ -638,17 +652,17 @@ def deploy(
                 frontend=frontend,
                 frontend=frontend,
                 backend=backend,
                 backend=backend,
                 zipping=zipping,
                 zipping=zipping,
-                loglevel=loglevel.subprocess_level(),
+                loglevel=config.loglevel.subprocess_level(),
                 upload_db_file=upload_db,
                 upload_db_file=upload_db,
             )
             )
         ),
         ),
-        regions=regions,
-        envs=envs,
+        regions=list(region),
+        envs=list(env),
         vmtype=vmtype,
         vmtype=vmtype,
         envfile=envfile,
         envfile=envfile,
         hostname=hostname,
         hostname=hostname,
         interactive=interactive,
         interactive=interactive,
-        loglevel=_convert_reflex_loglevel_to_reflex_cli_loglevel(loglevel),
+        loglevel=_convert_reflex_loglevel_to_reflex_cli_loglevel(config.loglevel),
         token=token,
         token=token,
         project=project,
         project=project,
         project_name=project_name,
         project_name=project_name,
@@ -657,19 +671,14 @@ def deploy(
 
 
 
 
 @cli.command()
 @cli.command()
-def rename(
-    new_name: str = typer.Argument(..., help="The new name for the app."),
-    loglevel: constants.LogLevel | None = typer.Option(
-        None, help="The log level to use."
-    ),
-):
+@loglevel_option
+@click.argument("new_name")
+def rename(new_name: str):
     """Rename the app in the current directory."""
     """Rename the app in the current directory."""
     from reflex.utils import prerequisites
     from reflex.utils import prerequisites
 
 
-    loglevel = loglevel or get_config().loglevel
-
     prerequisites.validate_app_name(new_name)
     prerequisites.validate_app_name(new_name)
-    prerequisites.rename_app(new_name, loglevel)
+    prerequisites.rename_app(new_name, get_config().loglevel)
 
 
 
 
 if TYPE_CHECKING:
 if TYPE_CHECKING:
@@ -702,18 +711,20 @@ def _convert_reflex_loglevel_to_reflex_cli_loglevel(
     return HostingLogLevel.INFO
     return HostingLogLevel.INFO
 
 
 
 
-cli.add_typer(db_cli, name="db", help="Subcommands for managing the database schema.")
-cli.add_typer(script_cli, name="script", help="Subcommands running helper scripts.")
-cli.add_typer(
-    hosting_cli,  # pyright: ignore [reportArgumentType]
-    name="cloud",
-    help="Subcommands for managing the reflex cloud.",
-)
-cli.add_typer(
-    custom_components_cli,
-    name="component",
-    help="Subcommands for creating and publishing Custom Components.",
-)
+if find_spec("typer"):
+    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__":
 if __name__ == "__main__":
     cli()
     cli()

+ 10 - 3
reflex/testing.py

@@ -34,7 +34,7 @@ import reflex.utils.format
 import reflex.utils.prerequisites
 import reflex.utils.prerequisites
 import reflex.utils.processes
 import reflex.utils.processes
 from reflex.components.component import CustomComponent
 from reflex.components.component import CustomComponent
-from reflex.config import environment
+from reflex.config import environment, get_config
 from reflex.state import (
 from reflex.state import (
     BaseState,
     BaseState,
     StateManager,
     StateManager,
@@ -44,6 +44,7 @@ from reflex.state import (
     reload_state_module,
     reload_state_module,
 )
 )
 from reflex.utils import console
 from reflex.utils import console
+from reflex.utils.export import export
 
 
 try:
 try:
     from selenium import webdriver
     from selenium import webdriver
@@ -252,11 +253,11 @@ class AppHarness:
                     self._get_source_from_app_source(self.app_source),
                     self._get_source_from_app_source(self.app_source),
                 ]
                 ]
             )
             )
+            get_config().loglevel = reflex.constants.LogLevel.INFO
             with chdir(self.app_path):
             with chdir(self.app_path):
                 reflex.reflex._init(
                 reflex.reflex._init(
                     name=self.app_name,
                     name=self.app_name,
                     template=reflex.constants.Templates.DEFAULT,
                     template=reflex.constants.Templates.DEFAULT,
-                    loglevel=reflex.constants.LogLevel.INFO,
                 )
                 )
                 self.app_module_path.write_text(source_code)
                 self.app_module_path.write_text(source_code)
         else:
         else:
@@ -933,7 +934,13 @@ class AppHarnessProd(AppHarness):
             config.api_url = "http://{}:{}".format(
             config.api_url = "http://{}:{}".format(
                 *self._poll_for_servers().getsockname(),
                 *self._poll_for_servers().getsockname(),
             )
             )
-            reflex.reflex.export(
+
+            get_config().loglevel = reflex.constants.LogLevel.INFO
+
+            if reflex.utils.prerequisites.needs_reinit(frontend=True):
+                reflex.reflex._init(name=get_config().app_name)
+
+            export(
                 zipping=False,
                 zipping=False,
                 frontend=True,
                 frontend=True,
                 backend=False,
                 backend=False,

+ 4 - 3
reflex/utils/console.py

@@ -47,7 +47,7 @@ _EMITTED_LOGS = set()
 _EMITTED_PRINTS = set()
 _EMITTED_PRINTS = set()
 
 
 
 
-def set_log_level(log_level: LogLevel):
+def set_log_level(log_level: LogLevel | None):
     """Set the log level.
     """Set the log level.
 
 
     Args:
     Args:
@@ -56,6 +56,8 @@ def set_log_level(log_level: LogLevel):
     Raises:
     Raises:
         TypeError: If the log level is a string.
         TypeError: If the log level is a string.
     """
     """
+    if log_level is None:
+        return
     if not isinstance(log_level, LogLevel):
     if not isinstance(log_level, LogLevel):
         raise TypeError(
         raise TypeError(
             f"log_level must be a LogLevel enum value, got {log_level} of type {type(log_level)} instead."
             f"log_level must be a LogLevel enum value, got {log_level} of type {type(log_level)} instead."
@@ -193,13 +195,12 @@ def warn(msg: str, dedupe: bool = False, **kwargs):
 
 
 def _get_first_non_framework_frame() -> FrameType | None:
 def _get_first_non_framework_frame() -> FrameType | None:
     import click
     import click
-    import typer
     import typing_extensions
     import typing_extensions
 
 
     import reflex as rx
     import reflex as rx
 
 
     # Exclude utility modules that should never be the source of deprecated reflex usage.
     # Exclude utility modules that should never be the source of deprecated reflex usage.
-    exclude_modules = [click, rx, typer, typing_extensions]
+    exclude_modules = [click, rx, typing_extensions]
     exclude_roots = [
     exclude_roots = [
         p.parent.resolve() if (p := Path(file)).name == "__init__.py" else p.resolve()
         p.parent.resolve() if (p := Path(file)).name == "__init__.py" else p.resolve()
         for m in exclude_modules
         for m in exclude_modules

+ 27 - 27
reflex/utils/prerequisites.py

@@ -27,8 +27,8 @@ from types import ModuleType
 from typing import NamedTuple
 from typing import NamedTuple
 from urllib.parse import urlparse
 from urllib.parse import urlparse
 
 
+import click
 import httpx
 import httpx
-import typer
 from alembic.util.exc import CommandError
 from alembic.util.exc import CommandError
 from packaging import version
 from packaging import version
 from redis import Redis as RedisSync
 from redis import Redis as RedisSync
@@ -517,7 +517,7 @@ def compile_or_validate_app(compile: bool = False) -> bool:
         else:
         else:
             validate_app()
             validate_app()
     except Exception as e:
     except Exception as e:
-        if isinstance(e, typer.Exit):
+        if isinstance(e, click.exceptions.Exit):
             return False
             return False
 
 
         import traceback
         import traceback
@@ -621,14 +621,14 @@ def validate_app_name(app_name: str | None = None) -> str:
         console.error(
         console.error(
             f"The app directory cannot be named [bold]{constants.Reflex.MODULE_NAME}[/bold]."
             f"The app directory cannot be named [bold]{constants.Reflex.MODULE_NAME}[/bold]."
         )
         )
-        raise typer.Exit(1)
+        raise click.exceptions.Exit(1)
 
 
     # Make sure the app name is standard for a python package name.
     # Make sure the app name is standard for a python package name.
     if not re.match(r"^[a-zA-Z][a-zA-Z0-9_]*$", app_name):
     if not re.match(r"^[a-zA-Z][a-zA-Z0-9_]*$", app_name):
         console.error(
         console.error(
             "The app directory name must start with a letter and can contain letters, numbers, and underscores."
             "The app directory name must start with a letter and can contain letters, numbers, and underscores."
         )
         )
-        raise typer.Exit(1)
+        raise click.exceptions.Exit(1)
 
 
     return app_name
     return app_name
 
 
@@ -687,7 +687,7 @@ def rename_app(new_app_name: str, loglevel: constants.LogLevel):
         console.error(
         console.error(
             "No rxconfig.py found. Make sure you are in the root directory of your app."
             "No rxconfig.py found. Make sure you are in the root directory of your app."
         )
         )
-        raise typer.Exit(1)
+        raise click.exceptions.Exit(1)
 
 
     sys.path.insert(0, str(Path.cwd()))
     sys.path.insert(0, str(Path.cwd()))
 
 
@@ -695,11 +695,11 @@ def rename_app(new_app_name: str, loglevel: constants.LogLevel):
     module_path = importlib.util.find_spec(config.module)
     module_path = importlib.util.find_spec(config.module)
     if module_path is None:
     if module_path is None:
         console.error(f"Could not find module {config.module}.")
         console.error(f"Could not find module {config.module}.")
-        raise typer.Exit(1)
+        raise click.exceptions.Exit(1)
 
 
     if not module_path.origin:
     if not module_path.origin:
         console.error(f"Could not find origin for module {config.module}.")
         console.error(f"Could not find origin for module {config.module}.")
-        raise typer.Exit(1)
+        raise click.exceptions.Exit(1)
     console.info(f"Renaming app directory to {new_app_name}.")
     console.info(f"Renaming app directory to {new_app_name}.")
     process_directory(
     process_directory(
         Path.cwd(),
         Path.cwd(),
@@ -862,7 +862,7 @@ def initialize_requirements_txt() -> bool:
             continue
             continue
         except Exception as e:
         except Exception as e:
             console.error(f"Failed to read {requirements_file_path}.")
             console.error(f"Failed to read {requirements_file_path}.")
-            raise typer.Exit(1) from e
+            raise click.exceptions.Exit(1) from e
     else:
     else:
         return False
         return False
 
 
@@ -907,7 +907,7 @@ def initialize_app_directory(
             console.error(
             console.error(
                 f"Only {template_name=} should be provided, got {template_code_dir_name=}, {template_dir=}."
                 f"Only {template_name=} should be provided, got {template_code_dir_name=}, {template_dir=}."
             )
             )
-            raise typer.Exit(1)
+            raise click.exceptions.Exit(1)
         template_code_dir_name = constants.Templates.Dirs.CODE
         template_code_dir_name = constants.Templates.Dirs.CODE
         template_dir = Path(constants.Templates.Dirs.BASE, "apps", template_name)
         template_dir = Path(constants.Templates.Dirs.BASE, "apps", template_name)
     else:
     else:
@@ -915,7 +915,7 @@ def initialize_app_directory(
             console.error(
             console.error(
                 f"For `{template_name}` template, `template_code_dir_name` and `template_dir` should both be provided."
                 f"For `{template_name}` template, `template_code_dir_name` and `template_dir` should both be provided."
             )
             )
-            raise typer.Exit(1)
+            raise click.exceptions.Exit(1)
 
 
     console.debug(f"Using {template_name=} {template_dir=} {template_code_dir_name=}.")
     console.debug(f"Using {template_name=} {template_dir=} {template_code_dir_name=}.")
 
 
@@ -1147,7 +1147,7 @@ def download_and_run(url: str, *args, show_status: bool = False, **env):
         console.error(
         console.error(
             f"Failed to download bun install script. You can install or update bun manually from https://bun.sh \n{e}"
             f"Failed to download bun install script. You can install or update bun manually from https://bun.sh \n{e}"
         )
         )
-        raise typer.Exit(1) from None
+        raise click.exceptions.Exit(1) from None
 
 
     # Save the script to a temporary file.
     # Save the script to a temporary file.
     script = Path(tempfile.NamedTemporaryFile().name)
     script = Path(tempfile.NamedTemporaryFile().name)
@@ -1381,7 +1381,7 @@ def needs_reinit(frontend: bool = True) -> bool:
         console.error(
         console.error(
             f"[cyan]{constants.Config.FILE}[/cyan] not found. Move to the root folder of your project, or run [bold]{constants.Reflex.MODULE_NAME} init[/bold] to start a new project."
             f"[cyan]{constants.Config.FILE}[/cyan] not found. Move to the root folder of your project, or run [bold]{constants.Reflex.MODULE_NAME} init[/bold] to start a new project."
         )
         )
-        raise typer.Exit(1)
+        raise click.exceptions.Exit(1)
 
 
     # Don't need to reinit if not running in frontend mode.
     # Don't need to reinit if not running in frontend mode.
     if not frontend:
     if not frontend:
@@ -1446,7 +1446,7 @@ def validate_bun(bun_path: Path | None = None):
             console.error(
             console.error(
                 "Failed to obtain bun version. Make sure the specified bun path in your config is correct."
                 "Failed to obtain bun version. Make sure the specified bun path in your config is correct."
             )
             )
-            raise typer.Exit(1)
+            raise click.exceptions.Exit(1)
         elif bun_version < version.parse(constants.Bun.MIN_VERSION):
         elif bun_version < version.parse(constants.Bun.MIN_VERSION):
             console.warn(
             console.warn(
                 f"Reflex requires bun version {constants.Bun.MIN_VERSION} or higher to run, but the detected version is "
                 f"Reflex requires bun version {constants.Bun.MIN_VERSION} or higher to run, but the detected version is "
@@ -1468,14 +1468,14 @@ def validate_frontend_dependencies(init: bool = True):
         try:
         try:
             get_js_package_executor(raise_on_none=True)
             get_js_package_executor(raise_on_none=True)
         except FileNotFoundError as e:
         except FileNotFoundError as e:
-            raise typer.Exit(1) from e
+            raise click.exceptions.Exit(1) from e
 
 
     if prefer_npm_over_bun() and not check_node_version():
     if prefer_npm_over_bun() and not check_node_version():
         node_version = get_node_version()
         node_version = get_node_version()
         console.error(
         console.error(
             f"Reflex requires node version {constants.Node.MIN_VERSION} or higher to run, but the detected version is {node_version}",
             f"Reflex requires node version {constants.Node.MIN_VERSION} or higher to run, but the detected version is {node_version}",
         )
         )
-        raise typer.Exit(1)
+        raise click.exceptions.Exit(1)
 
 
 
 
 def ensure_reflex_installation_id() -> int | None:
 def ensure_reflex_installation_id() -> int | None:
@@ -1622,17 +1622,17 @@ def prompt_for_template_options(templates: list[Template]) -> str:
 
 
     if not template:
     if not template:
         console.error("No template selected.")
         console.error("No template selected.")
-        raise typer.Exit(1)
+        raise click.exceptions.Exit(1)
 
 
     try:
     try:
         template_index = int(template)
         template_index = int(template)
     except ValueError:
     except ValueError:
         console.error("Invalid template selected.")
         console.error("Invalid template selected.")
-        raise typer.Exit(1) from None
+        raise click.exceptions.Exit(1) from None
 
 
     if template_index < 0 or template_index >= len(templates):
     if template_index < 0 or template_index >= len(templates):
         console.error("Invalid template selected.")
         console.error("Invalid template selected.")
-        raise typer.Exit(1)
+        raise click.exceptions.Exit(1)
 
 
     # Return the template.
     # Return the template.
     return templates[template_index].name
     return templates[template_index].name
@@ -1712,7 +1712,7 @@ def create_config_init_app_from_remote_template(app_name: str, template_url: str
         temp_dir = tempfile.mkdtemp()
         temp_dir = tempfile.mkdtemp()
     except OSError as ose:
     except OSError as ose:
         console.error(f"Failed to create temp directory for download: {ose}")
         console.error(f"Failed to create temp directory for download: {ose}")
-        raise typer.Exit(1) from ose
+        raise click.exceptions.Exit(1) from ose
 
 
     # Use httpx GET with redirects to download the zip file.
     # Use httpx GET with redirects to download the zip file.
     zip_file_path: Path = Path(temp_dir) / "template.zip"
     zip_file_path: Path = Path(temp_dir) / "template.zip"
@@ -1723,20 +1723,20 @@ def create_config_init_app_from_remote_template(app_name: str, template_url: str
         response.raise_for_status()
         response.raise_for_status()
     except httpx.HTTPError as he:
     except httpx.HTTPError as he:
         console.error(f"Failed to download the template: {he}")
         console.error(f"Failed to download the template: {he}")
-        raise typer.Exit(1) from he
+        raise click.exceptions.Exit(1) from he
     try:
     try:
         zip_file_path.write_bytes(response.content)
         zip_file_path.write_bytes(response.content)
         console.debug(f"Downloaded the zip to {zip_file_path}")
         console.debug(f"Downloaded the zip to {zip_file_path}")
     except OSError as ose:
     except OSError as ose:
         console.error(f"Unable to write the downloaded zip to disk {ose}")
         console.error(f"Unable to write the downloaded zip to disk {ose}")
-        raise typer.Exit(1) from ose
+        raise click.exceptions.Exit(1) from ose
 
 
     # Create a temp directory for the zip extraction.
     # Create a temp directory for the zip extraction.
     try:
     try:
         unzip_dir = Path(tempfile.mkdtemp())
         unzip_dir = Path(tempfile.mkdtemp())
     except OSError as ose:
     except OSError as ose:
         console.error(f"Failed to create temp directory for extracting zip: {ose}")
         console.error(f"Failed to create temp directory for extracting zip: {ose}")
-        raise typer.Exit(1) from ose
+        raise click.exceptions.Exit(1) from ose
 
 
     try:
     try:
         zipfile.ZipFile(zip_file_path).extractall(path=unzip_dir)
         zipfile.ZipFile(zip_file_path).extractall(path=unzip_dir)
@@ -1744,11 +1744,11 @@ def create_config_init_app_from_remote_template(app_name: str, template_url: str
         # repo-name-branch/**/*, so we need to remove the top level directory.
         # repo-name-branch/**/*, so we need to remove the top level directory.
     except Exception as uze:
     except Exception as uze:
         console.error(f"Failed to unzip the template: {uze}")
         console.error(f"Failed to unzip the template: {uze}")
-        raise typer.Exit(1) from uze
+        raise click.exceptions.Exit(1) from uze
 
 
     if len(subdirs := list(unzip_dir.iterdir())) != 1:
     if len(subdirs := list(unzip_dir.iterdir())) != 1:
         console.error(f"Expected one directory in the zip, found {subdirs}")
         console.error(f"Expected one directory in the zip, found {subdirs}")
-        raise typer.Exit(1)
+        raise click.exceptions.Exit(1)
 
 
     template_dir = unzip_dir / subdirs[0]
     template_dir = unzip_dir / subdirs[0]
     console.debug(f"Template folder is located at {template_dir}")
     console.debug(f"Template folder is located at {template_dir}")
@@ -1810,7 +1810,7 @@ def validate_and_create_app_using_remote_template(
             console.print(
             console.print(
                 f"Please use `reflex login` to access the '{template}' template."
                 f"Please use `reflex login` to access the '{template}' template."
             )
             )
-            raise typer.Exit(3)
+            raise click.exceptions.Exit(3)
 
 
         template_url = templates[template].code_url
         template_url = templates[template].code_url
     else:
     else:
@@ -1821,7 +1821,7 @@ def validate_and_create_app_using_remote_template(
             template_url = f"https://github.com/{path}/archive/main.zip"
             template_url = f"https://github.com/{path}/archive/main.zip"
         else:
         else:
             console.error(f"Template `{template}` not found or invalid.")
             console.error(f"Template `{template}` not found or invalid.")
-            raise typer.Exit(1)
+            raise click.exceptions.Exit(1)
 
 
     if template_url is None:
     if template_url is None:
         return
         return
@@ -1888,7 +1888,7 @@ def initialize_app(app_name: str, template: str | None = None) -> str | None:
             console.print(
             console.print(
                 f"Go to the templates page ({constants.Templates.REFLEX_TEMPLATES_URL}) and copy the command to init with a template."
                 f"Go to the templates page ({constants.Templates.REFLEX_TEMPLATES_URL}) and copy the command to init with a template."
             )
             )
-            raise typer.Exit(0)
+            raise click.exceptions.Exit(0)
 
 
     # If the blank template is selected, create a blank app.
     # If the blank template is selected, create a blank app.
     if template in (constants.Templates.DEFAULT,):
     if template in (constants.Templates.DEFAULT,):

+ 6 - 6
reflex/utils/processes.py

@@ -13,8 +13,8 @@ from concurrent import futures
 from pathlib import Path
 from pathlib import Path
 from typing import Any, Literal, overload
 from typing import Any, Literal, overload
 
 
+import click
 import psutil
 import psutil
-import typer
 from redis.exceptions import RedisError
 from redis.exceptions import RedisError
 from rich.progress import Progress
 from rich.progress import Progress
 
 
@@ -48,7 +48,7 @@ def get_num_workers() -> int:
         redis_client.ping()
         redis_client.ping()
     except RedisError as re:
     except RedisError as re:
         console.error(f"Unable to connect to Redis: {re}")
         console.error(f"Unable to connect to Redis: {re}")
-        raise typer.Exit(1) from re
+        raise click.exceptions.Exit(1) from re
     return (os.cpu_count() or 1) * 2 + 1
     return (os.cpu_count() or 1) * 2 + 1
 
 
 
 
@@ -141,7 +141,7 @@ def handle_port(service_name: str, port: int, auto_increment: bool) -> int:
         console.error(
         console.error(
             f"{service_name.capitalize()} port: {port} is already in use by PID: {process.pid}."
             f"{service_name.capitalize()} port: {port} is already in use by PID: {process.pid}."
         )
         )
-        raise typer.Exit()
+        raise click.exceptions.Exit()
 
 
 
 
 @overload
 @overload
@@ -186,7 +186,7 @@ def new_process(
     non_empty_args = list(filter(None, args)) if isinstance(args, list) else [args]
     non_empty_args = list(filter(None, args)) if isinstance(args, list) else [args]
     if isinstance(args, list) and len(non_empty_args) != len(args):
     if isinstance(args, list) and len(non_empty_args) != len(args):
         console.error(f"Invalid command: {args}")
         console.error(f"Invalid command: {args}")
-        raise typer.Exit(1)
+        raise click.exceptions.Exit(1)
 
 
     path_env: str = os.environ.get("PATH", "")
     path_env: str = os.environ.get("PATH", "")
 
 
@@ -345,7 +345,7 @@ def stream_logs(
                 "NPM_CONFIG_REGISTRY environment variable. If TLS is the issue, and you know what "
                 "NPM_CONFIG_REGISTRY environment variable. If TLS is the issue, and you know what "
                 "you are doing, you can disable it by setting the SSL_NO_VERIFY environment variable."
                 "you are doing, you can disable it by setting the SSL_NO_VERIFY environment variable."
             )
             )
-            raise typer.Exit(1)
+            raise click.exceptions.Exit(1)
         for set_of_logs in (*prior_logs, tuple(logs)):
         for set_of_logs in (*prior_logs, tuple(logs)):
             for line in set_of_logs:
             for line in set_of_logs:
                 console.error(line, end="")
                 console.error(line, end="")
@@ -353,7 +353,7 @@ def stream_logs(
         if analytics_enabled:
         if analytics_enabled:
             telemetry.send("error", context=message)
             telemetry.send("error", context=message)
         console.error("Run with [bold]--loglevel debug [/bold] for the full log.")
         console.error("Run with [bold]--loglevel debug [/bold] for the full log.")
-        raise typer.Exit(1)
+        raise click.exceptions.Exit(1)
 
 
 
 
 def show_logs(message: str, process: subprocess.Popen):
 def show_logs(message: str, process: subprocess.Popen):

+ 2 - 2
tests/units/test_prerequisites.py

@@ -5,7 +5,7 @@ import tempfile
 from pathlib import Path
 from pathlib import Path
 
 
 import pytest
 import pytest
-from typer.testing import CliRunner
+from click.testing import CliRunner
 
 
 from reflex.config import Config
 from reflex.config import Config
 from reflex.reflex import cli
 from reflex.reflex import cli
@@ -279,7 +279,7 @@ app.add_page(index)
     with chdir(temp_directory / "foo"):
     with chdir(temp_directory / "foo"):
         result = runner.invoke(cli, ["rename", "bar"])
         result = runner.invoke(cli, ["rename", "bar"])
 
 
-    assert result.exit_code == 0
+    assert result.exit_code == 0, result.output
     assert (foo_dir / "rxconfig.py").read_text() == (
     assert (foo_dir / "rxconfig.py").read_text() == (
         """
         """
 import reflex as rx
 import reflex as rx

+ 5 - 5
tests/units/utils/test_utils.py

@@ -5,8 +5,8 @@ from functools import cached_property
 from pathlib import Path
 from pathlib import Path
 from typing import Any, ClassVar, List, Literal, NoReturn  # noqa: UP035
 from typing import Any, ClassVar, List, Literal, NoReturn  # noqa: UP035
 
 
+import click
 import pytest
 import pytest
-import typer
 from packaging import version
 from packaging import version
 
 
 from reflex import constants
 from reflex import constants
@@ -180,7 +180,7 @@ def test_validate_none_bun_path(mocker):
         mocker: Pytest mocker object.
         mocker: Pytest mocker object.
     """
     """
     mocker.patch("reflex.utils.path_ops.get_bun_path", return_value=None)
     mocker.patch("reflex.utils.path_ops.get_bun_path", return_value=None)
-    # with pytest.raises(typer.Exit):
+    # with pytest.raises(click.exceptions.Exit):
     prerequisites.validate_bun()
     prerequisites.validate_bun()
 
 
 
 
@@ -198,7 +198,7 @@ def test_validate_invalid_bun_path(
     mocker.patch("reflex.utils.path_ops.samefile", return_value=False)
     mocker.patch("reflex.utils.path_ops.samefile", return_value=False)
     mocker.patch("reflex.utils.prerequisites.get_bun_version", return_value=None)
     mocker.patch("reflex.utils.prerequisites.get_bun_version", return_value=None)
 
 
-    with pytest.raises(typer.Exit):
+    with pytest.raises(click.exceptions.Exit):
         prerequisites.validate_bun()
         prerequisites.validate_bun()
 
 
 
 
@@ -464,10 +464,10 @@ def test_validate_app_name(tmp_path, mocker):
 
 
     mocker.patch("reflex.utils.prerequisites.os.getcwd", return_value=str(reflex))
     mocker.patch("reflex.utils.prerequisites.os.getcwd", return_value=str(reflex))
 
 
-    with pytest.raises(typer.Exit):
+    with pytest.raises(click.exceptions.Exit):
         prerequisites.validate_app_name()
         prerequisites.validate_app_name()
 
 
-    with pytest.raises(typer.Exit):
+    with pytest.raises(click.exceptions.Exit):
         prerequisites.validate_app_name(app_name="1_test")
         prerequisites.validate_app_name(app_name="1_test")
 
 
 
 

+ 3 - 27
uv.lock

@@ -1514,6 +1514,7 @@ version = "0.7.9.dev1"
 source = { editable = "." }
 source = { editable = "." }
 dependencies = [
 dependencies = [
     { name = "alembic" },
     { name = "alembic" },
+    { name = "click" },
     { name = "fastapi" },
     { name = "fastapi" },
     { name = "granian", extra = ["reload"] },
     { name = "granian", extra = ["reload"] },
     { name = "httpx" },
     { name = "httpx" },
@@ -1528,7 +1529,6 @@ dependencies = [
     { name = "reflex-hosting-cli" },
     { name = "reflex-hosting-cli" },
     { name = "rich" },
     { name = "rich" },
     { name = "sqlmodel" },
     { name = "sqlmodel" },
-    { name = "typer" },
     { name = "typing-extensions" },
     { name = "typing-extensions" },
     { name = "wrapt" },
     { name = "wrapt" },
 ]
 ]
@@ -1567,6 +1567,7 @@ dev = [
 [package.metadata]
 [package.metadata]
 requires-dist = [
 requires-dist = [
     { name = "alembic", specifier = ">=1.15.2,<2.0" },
     { name = "alembic", specifier = ">=1.15.2,<2.0" },
+    { name = "click", specifier = ">=8" },
     { name = "fastapi", specifier = ">=0.115.0" },
     { name = "fastapi", specifier = ">=0.115.0" },
     { name = "granian", extras = ["reload"], specifier = ">=2.2.5" },
     { name = "granian", extras = ["reload"], specifier = ">=2.2.5" },
     { name = "httpx", specifier = ">=0.28.0,<1.0" },
     { name = "httpx", specifier = ">=0.28.0,<1.0" },
@@ -1578,10 +1579,9 @@ requires-dist = [
     { name = "python-multipart", specifier = ">=0.0.20,<1.0" },
     { name = "python-multipart", specifier = ">=0.0.20,<1.0" },
     { name = "python-socketio", specifier = ">=5.12.0,<6.0" },
     { name = "python-socketio", specifier = ">=5.12.0,<6.0" },
     { name = "redis", specifier = ">=5.2.1,<6.0" },
     { name = "redis", specifier = ">=5.2.1,<6.0" },
-    { name = "reflex-hosting-cli", specifier = ">=0.1.38" },
+    { name = "reflex-hosting-cli", specifier = ">=0.1.43" },
     { name = "rich", specifier = ">=13,<15" },
     { name = "rich", specifier = ">=13,<15" },
     { name = "sqlmodel", specifier = ">=0.0.24,<0.1" },
     { name = "sqlmodel", specifier = ">=0.0.24,<0.1" },
-    { name = "typer", specifier = ">=0.15.2,<1.0" },
     { name = "typing-extensions", specifier = ">=4.13.0" },
     { name = "typing-extensions", specifier = ">=4.13.0" },
     { name = "wrapt", specifier = ">=1.17.0,<2.0" },
     { name = "wrapt", specifier = ">=1.17.0,<2.0" },
 ]
 ]
@@ -1706,15 +1706,6 @@ wheels = [
     { url = "https://files.pythonhosted.org/packages/32/53/212db779d2481b0a8428365960596f8d5a4d482ae12c441d0507fd54aaf2/selenium-4.31.0-py3-none-any.whl", hash = "sha256:7b8b8d5e424d7133cb7aa656263b19ac505ec26d65c0f921a696e7e2c5ccd95b", size = 9350584, upload_time = "2025-04-05T00:43:04.04Z" },
     { url = "https://files.pythonhosted.org/packages/32/53/212db779d2481b0a8428365960596f8d5a4d482ae12c441d0507fd54aaf2/selenium-4.31.0-py3-none-any.whl", hash = "sha256:7b8b8d5e424d7133cb7aa656263b19ac505ec26d65c0f921a696e7e2c5ccd95b", size = 9350584, upload_time = "2025-04-05T00:43:04.04Z" },
 ]
 ]
 
 
-[[package]]
-name = "shellingham"
-version = "1.5.4"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload_time = "2023-10-24T04:13:40.426Z" }
-wheels = [
-    { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload_time = "2023-10-24T04:13:38.866Z" },
-]
-
 [[package]]
 [[package]]
 name = "simple-websocket"
 name = "simple-websocket"
 version = "1.1.0"
 version = "1.1.0"
@@ -1937,21 +1928,6 @@ wheels = [
     { url = "https://files.pythonhosted.org/packages/70/7d/a2271b98b833680561ab3fcd60ab682478dc4f7cc023fab24991601ac8ac/trove_classifiers-2025.4.11.15-py3-none-any.whl", hash = "sha256:e7d98983f004df35293caf954bdfe944b139eb402677a97115450e320f0bd855", size = 13710, upload_time = "2025-04-11T15:13:16.152Z" },
     { url = "https://files.pythonhosted.org/packages/70/7d/a2271b98b833680561ab3fcd60ab682478dc4f7cc023fab24991601ac8ac/trove_classifiers-2025.4.11.15-py3-none-any.whl", hash = "sha256:e7d98983f004df35293caf954bdfe944b139eb402677a97115450e320f0bd855", size = 13710, upload_time = "2025-04-11T15:13:16.152Z" },
 ]
 ]
 
 
-[[package]]
-name = "typer"
-version = "0.15.2"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
-    { name = "click" },
-    { name = "rich" },
-    { name = "shellingham" },
-    { name = "typing-extensions" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/8b/6f/3991f0f1c7fcb2df31aef28e0594d8d54b05393a0e4e34c65e475c2a5d41/typer-0.15.2.tar.gz", hash = "sha256:ab2fab47533a813c49fe1f16b1a370fd5819099c00b119e0633df65f22144ba5", size = 100711, upload_time = "2025-02-27T19:17:34.807Z" }
-wheels = [
-    { url = "https://files.pythonhosted.org/packages/7f/fc/5b29fea8cee020515ca82cc68e3b8e1e34bb19a3535ad854cac9257b414c/typer-0.15.2-py3-none-any.whl", hash = "sha256:46a499c6107d645a9c13f7ee46c5d5096cae6f5fc57dd11eccbbb9ae3e44ddfc", size = 45061, upload_time = "2025-02-27T19:17:32.111Z" },
-]
-
 [[package]]
 [[package]]
 name = "typing-extensions"
 name = "typing-extensions"
 version = "4.13.2"
 version = "4.13.2"