reflex.py 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761
  1. """Reflex CLI to create, run, and deploy apps."""
  2. from __future__ import annotations
  3. import atexit
  4. from importlib.util import find_spec
  5. from pathlib import Path
  6. from typing import TYPE_CHECKING
  7. import click
  8. from reflex_cli.v2.deployments import hosting_cli
  9. from reflex import constants
  10. from reflex.config import environment, get_config
  11. from reflex.constants.base import LITERAL_ENV
  12. from reflex.custom_components.custom_components import custom_components_cli
  13. from reflex.state import reset_disk_state_manager
  14. from reflex.utils import console, redir, telemetry
  15. from reflex.utils.exec import should_use_granian
  16. def set_loglevel(ctx: click.Context, self: click.Parameter, value: str | None):
  17. """Set the log level.
  18. Args:
  19. ctx: The click context.
  20. self: The click command.
  21. value: The log level to set.
  22. """
  23. if value is not None:
  24. loglevel = constants.LogLevel.from_string(value)
  25. console.set_log_level(loglevel)
  26. @click.group
  27. @click.version_option(constants.Reflex.VERSION, message="%(version)s")
  28. def cli():
  29. """Reflex CLI to create, run, and deploy apps."""
  30. pass
  31. loglevel_option = click.option(
  32. "--loglevel",
  33. type=click.Choice(
  34. [loglevel.value for loglevel in constants.LogLevel],
  35. case_sensitive=False,
  36. ),
  37. is_eager=True,
  38. callback=set_loglevel,
  39. expose_value=False,
  40. help="The log level to use.",
  41. )
  42. def _init(
  43. name: str,
  44. template: str | None = None,
  45. ai: bool = False,
  46. ):
  47. """Initialize a new Reflex app in the given directory."""
  48. from reflex.utils import exec, prerequisites
  49. # Show system info
  50. exec.output_system_info()
  51. if ai:
  52. redir.reflex_build_redirect()
  53. return
  54. # Validate the app name.
  55. app_name = prerequisites.validate_app_name(name)
  56. console.rule(f"[bold]Initializing {app_name}")
  57. # Check prerequisites.
  58. prerequisites.check_latest_package_version(constants.Reflex.MODULE_NAME)
  59. prerequisites.initialize_reflex_user_directory()
  60. prerequisites.ensure_reflex_installation_id()
  61. # Set up the web project.
  62. prerequisites.initialize_frontend_dependencies()
  63. # Initialize the app.
  64. template = prerequisites.initialize_app(app_name, template)
  65. # Initialize the .gitignore.
  66. prerequisites.initialize_gitignore()
  67. # Initialize the requirements.txt.
  68. needs_user_manual_update = prerequisites.initialize_requirements_txt()
  69. template_msg = f" using the {template} template" if template else ""
  70. # Finish initializing the app.
  71. console.success(
  72. f"Initialized {app_name}{template_msg}."
  73. + (
  74. f" Make sure to add {constants.RequirementsTxt.DEFAULTS_STUB + constants.Reflex.VERSION} to your requirements.txt or pyproject.toml file."
  75. if needs_user_manual_update
  76. else ""
  77. )
  78. )
  79. @cli.command()
  80. @loglevel_option
  81. @click.option(
  82. "--name",
  83. metavar="APP_NAME",
  84. help="The name of the app to initialize.",
  85. )
  86. @click.option(
  87. "--template",
  88. help="The template to initialize the app with.",
  89. )
  90. @click.option(
  91. "--ai",
  92. is_flag=True,
  93. help="Use AI to create the initial template. Cannot be used with existing app or `--template` option.",
  94. )
  95. def init(
  96. name: str,
  97. template: str | None,
  98. ai: bool,
  99. ):
  100. """Initialize a new Reflex app in the current directory."""
  101. _init(name, template, ai)
  102. def _run(
  103. env: constants.Env = constants.Env.DEV,
  104. frontend: bool = True,
  105. backend: bool = True,
  106. frontend_port: int | None = None,
  107. backend_port: int | None = None,
  108. backend_host: str | None = None,
  109. ):
  110. """Run the app in the given directory."""
  111. from reflex.utils import build, exec, prerequisites, processes
  112. config = get_config()
  113. backend_host = backend_host or config.backend_host
  114. # Set env mode in the environment
  115. environment.REFLEX_ENV_MODE.set(env)
  116. # Show system info
  117. exec.output_system_info()
  118. # If no --frontend-only and no --backend-only, then turn on frontend and backend both
  119. frontend, backend = prerequisites.check_running_mode(frontend, backend)
  120. if not frontend and backend:
  121. _skip_compile()
  122. prerequisites.assert_in_reflex_dir()
  123. # Check that the app is initialized.
  124. if frontend and prerequisites.needs_reinit():
  125. _init(name=config.app_name)
  126. # Delete the states folder if it exists.
  127. reset_disk_state_manager()
  128. # Find the next available open port if applicable.
  129. if frontend:
  130. auto_increment_frontend = not bool(frontend_port or config.frontend_port)
  131. frontend_port = processes.handle_port(
  132. "frontend",
  133. (
  134. frontend_port
  135. or config.frontend_port
  136. or constants.DefaultPorts.FRONTEND_PORT
  137. ),
  138. auto_increment=auto_increment_frontend,
  139. )
  140. if backend:
  141. auto_increment_backend = not bool(backend_port or config.backend_port)
  142. backend_port = processes.handle_port(
  143. "backend",
  144. (
  145. backend_port
  146. or config.backend_port
  147. or constants.DefaultPorts.BACKEND_PORT
  148. ),
  149. auto_increment=auto_increment_backend,
  150. )
  151. # Apply the new ports to the config.
  152. if frontend_port != config.frontend_port:
  153. config._set_persistent(frontend_port=frontend_port)
  154. if backend_port != config.backend_port:
  155. config._set_persistent(backend_port=backend_port)
  156. # Reload the config to make sure the env vars are persistent.
  157. get_config(reload=True)
  158. console.rule("[bold]Starting Reflex App")
  159. prerequisites.check_latest_package_version(constants.Reflex.MODULE_NAME)
  160. # Get the app module.
  161. app_task = prerequisites.compile_or_validate_app
  162. args = (frontend,)
  163. # Granian fails if the app is already imported.
  164. if should_use_granian():
  165. import concurrent.futures
  166. compile_future = concurrent.futures.ProcessPoolExecutor(max_workers=1).submit(
  167. app_task,
  168. *args,
  169. )
  170. validation_result = compile_future.result()
  171. else:
  172. validation_result = app_task(*args)
  173. if not validation_result:
  174. raise click.exceptions.Exit(1)
  175. # Warn if schema is not up to date.
  176. prerequisites.check_schema_up_to_date()
  177. # Get the frontend and backend commands, based on the environment.
  178. setup_frontend = frontend_cmd = backend_cmd = None
  179. if env == constants.Env.DEV:
  180. setup_frontend, frontend_cmd, backend_cmd = (
  181. build.setup_frontend,
  182. exec.run_frontend,
  183. exec.run_backend,
  184. )
  185. if env == constants.Env.PROD:
  186. setup_frontend, frontend_cmd, backend_cmd = (
  187. build.setup_frontend_prod,
  188. exec.run_frontend_prod,
  189. exec.run_backend_prod,
  190. )
  191. if not setup_frontend or not frontend_cmd or not backend_cmd:
  192. raise ValueError(f"Invalid env: {env}. Must be DEV or PROD.")
  193. # Post a telemetry event.
  194. telemetry.send(f"run-{env.value}")
  195. # Display custom message when there is a keyboard interrupt.
  196. atexit.register(processes.atexit_handler)
  197. # Run the frontend and backend together.
  198. commands = []
  199. # Run the frontend on a separate thread.
  200. if frontend:
  201. setup_frontend(Path.cwd())
  202. commands.append((frontend_cmd, Path.cwd(), frontend_port, backend))
  203. # In prod mode, run the backend on a separate thread.
  204. if backend and env == constants.Env.PROD:
  205. commands.append(
  206. (
  207. backend_cmd,
  208. backend_host,
  209. backend_port,
  210. config.loglevel.subprocess_level(),
  211. frontend,
  212. )
  213. )
  214. # Start the frontend and backend.
  215. with processes.run_concurrently_context(*commands):
  216. # In dev mode, run the backend on the main thread.
  217. if backend and backend_port and env == constants.Env.DEV:
  218. backend_cmd(
  219. backend_host,
  220. int(backend_port),
  221. config.loglevel.subprocess_level(),
  222. frontend,
  223. )
  224. # The windows uvicorn bug workaround
  225. # https://github.com/reflex-dev/reflex/issues/2335
  226. if constants.IS_WINDOWS and exec.frontend_process:
  227. # Sends SIGTERM in windows
  228. exec.kill(exec.frontend_process.pid)
  229. @cli.command()
  230. @loglevel_option
  231. @click.option(
  232. "--env",
  233. type=click.Choice([e.value for e in constants.Env], case_sensitive=False),
  234. default=constants.Env.DEV.value,
  235. help="The environment to run the app in.",
  236. )
  237. @click.option(
  238. "--frontend-only",
  239. is_flag=True,
  240. show_default=False,
  241. help="Execute only frontend.",
  242. envvar=environment.REFLEX_FRONTEND_ONLY.name,
  243. )
  244. @click.option(
  245. "--backend-only",
  246. is_flag=True,
  247. show_default=False,
  248. help="Execute only backend.",
  249. envvar=environment.REFLEX_BACKEND_ONLY.name,
  250. )
  251. @click.option(
  252. "--frontend-port",
  253. type=int,
  254. help="Specify a different frontend port.",
  255. envvar=environment.REFLEX_FRONTEND_PORT.name,
  256. )
  257. @click.option(
  258. "--backend-port",
  259. type=int,
  260. help="Specify a different backend port.",
  261. envvar=environment.REFLEX_BACKEND_PORT.name,
  262. )
  263. @click.option(
  264. "--backend-host",
  265. help="Specify the backend host.",
  266. )
  267. def run(
  268. env: LITERAL_ENV,
  269. frontend_only: bool,
  270. backend_only: bool,
  271. frontend_port: int | None,
  272. backend_port: int | None,
  273. backend_host: str | None,
  274. ):
  275. """Run the app in the current directory."""
  276. if frontend_only and backend_only:
  277. console.error("Cannot use both --frontend-only and --backend-only options.")
  278. raise click.exceptions.Exit(1)
  279. config = get_config()
  280. frontend_port = frontend_port or config.frontend_port
  281. backend_port = backend_port or config.backend_port
  282. backend_host = backend_host or config.backend_host
  283. environment.REFLEX_COMPILE_CONTEXT.set(constants.CompileContext.RUN)
  284. environment.REFLEX_BACKEND_ONLY.set(backend_only)
  285. environment.REFLEX_FRONTEND_ONLY.set(frontend_only)
  286. _run(
  287. constants.Env.DEV if env == constants.Env.DEV else constants.Env.PROD,
  288. frontend_only,
  289. backend_only,
  290. frontend_port,
  291. backend_port,
  292. backend_host,
  293. )
  294. @cli.command()
  295. @loglevel_option
  296. @click.option(
  297. "--dry",
  298. is_flag=True,
  299. default=False,
  300. help="Run the command without making any changes.",
  301. )
  302. def compile(dry: bool):
  303. """Compile the app in the current directory."""
  304. import time
  305. from reflex.utils import prerequisites
  306. # Check the app.
  307. if prerequisites.needs_reinit():
  308. _init(name=get_config().app_name)
  309. get_config(reload=True)
  310. starting_time = time.monotonic()
  311. prerequisites.compile_app(dry_run=dry)
  312. elapsed_time = time.monotonic() - starting_time
  313. console.success(f"App compiled successfully in {elapsed_time:.3f} seconds.")
  314. @cli.command()
  315. @loglevel_option
  316. @click.option(
  317. "--zip/--no-zip",
  318. default=True,
  319. is_flag=True,
  320. help="Whether to zip the backend and frontend exports.",
  321. )
  322. @click.option(
  323. "--frontend-only",
  324. is_flag=True,
  325. show_default=False,
  326. envvar=environment.REFLEX_FRONTEND_ONLY.name,
  327. help="Export only frontend.",
  328. )
  329. @click.option(
  330. "--backend-only",
  331. is_flag=True,
  332. show_default=False,
  333. envvar=environment.REFLEX_BACKEND_ONLY.name,
  334. help="Export only backend.",
  335. )
  336. @click.option(
  337. "--zip-dest-dir",
  338. default=str(Path.cwd()),
  339. help="The directory to export the zip files to.",
  340. show_default=False,
  341. )
  342. @click.option(
  343. "--upload-db-file",
  344. is_flag=True,
  345. help="Whether to exclude sqlite db files when exporting backend.",
  346. hidden=True,
  347. )
  348. @click.option(
  349. "--env",
  350. type=click.Choice([e.value for e in constants.Env], case_sensitive=False),
  351. default=constants.Env.PROD.value,
  352. help="The environment to export the app in.",
  353. )
  354. def export(
  355. zip: bool,
  356. frontend_only: bool,
  357. backend_only: bool,
  358. zip_dest_dir: str,
  359. upload_db_file: bool,
  360. env: LITERAL_ENV,
  361. ):
  362. """Export the app to a zip file."""
  363. from reflex.utils import export as export_utils
  364. from reflex.utils import prerequisites
  365. environment.REFLEX_COMPILE_CONTEXT.set(constants.CompileContext.EXPORT)
  366. should_frontend_run, should_backend_run = prerequisites.check_running_mode(
  367. frontend_only, backend_only
  368. )
  369. config = get_config()
  370. prerequisites.assert_in_reflex_dir()
  371. if should_frontend_run and prerequisites.needs_reinit():
  372. _init(name=config.app_name)
  373. export_utils.export(
  374. zipping=zip,
  375. frontend=should_frontend_run,
  376. backend=should_backend_run,
  377. zip_dest_dir=zip_dest_dir,
  378. upload_db_file=upload_db_file,
  379. env=constants.Env.DEV if env == constants.Env.DEV else constants.Env.PROD,
  380. loglevel=config.loglevel.subprocess_level(),
  381. )
  382. @cli.command()
  383. @loglevel_option
  384. def login():
  385. """Authenticate with experimental Reflex hosting service."""
  386. from reflex_cli.v2 import cli as hosting_cli
  387. from reflex_cli.v2.deployments import check_version
  388. check_version()
  389. validated_info = hosting_cli.login()
  390. if validated_info is not None:
  391. _skip_compile() # Allow running outside of an app dir
  392. telemetry.send("login", user_uuid=validated_info.get("user_id"))
  393. @cli.command()
  394. @loglevel_option
  395. def logout():
  396. """Log out of access to Reflex hosting service."""
  397. from reflex_cli.v2.cli import logout
  398. from reflex_cli.v2.deployments import check_version
  399. check_version()
  400. logout(_convert_reflex_loglevel_to_reflex_cli_loglevel(get_config().loglevel))
  401. @click.group
  402. def db_cli():
  403. """Subcommands for managing the database schema."""
  404. pass
  405. @click.group
  406. def script_cli():
  407. """Subcommands for running helper scripts."""
  408. pass
  409. def _skip_compile():
  410. """Skip the compile step."""
  411. environment.REFLEX_SKIP_COMPILE.set(True)
  412. @db_cli.command(name="init")
  413. def db_init():
  414. """Create database schema and migration configuration."""
  415. from reflex import model
  416. from reflex.utils import prerequisites
  417. config = get_config()
  418. # Check the database url.
  419. if config.db_url is None:
  420. console.error("db_url is not configured, cannot initialize.")
  421. return
  422. # Check the alembic config.
  423. if environment.ALEMBIC_CONFIG.get().exists():
  424. console.error(
  425. "Database is already initialized. Use "
  426. "[bold]reflex db makemigrations[/bold] to create schema change "
  427. "scripts and [bold]reflex db migrate[/bold] to apply migrations "
  428. "to a new or existing database.",
  429. )
  430. return
  431. # Initialize the database.
  432. _skip_compile()
  433. prerequisites.get_compiled_app()
  434. model.Model.alembic_init()
  435. model.Model.migrate(autogenerate=True)
  436. @db_cli.command()
  437. def migrate():
  438. """Create or update database schema from migration scripts."""
  439. from reflex import model
  440. from reflex.utils import prerequisites
  441. # TODO see if we can use `get_app()` instead (no compile). Would _skip_compile still be needed then?
  442. _skip_compile()
  443. prerequisites.get_compiled_app()
  444. if not prerequisites.check_db_initialized():
  445. return
  446. model.Model.migrate()
  447. prerequisites.check_schema_up_to_date()
  448. @db_cli.command()
  449. @click.option(
  450. "--message",
  451. help="Human readable identifier for the generated revision.",
  452. )
  453. def makemigrations(message: str | None):
  454. """Create autogenerated alembic migration scripts."""
  455. from alembic.util.exc import CommandError
  456. from reflex import model
  457. from reflex.utils import prerequisites
  458. # TODO see if we can use `get_app()` instead (no compile). Would _skip_compile still be needed then?
  459. _skip_compile()
  460. prerequisites.get_compiled_app()
  461. if not prerequisites.check_db_initialized():
  462. return
  463. with model.Model.get_db_engine().connect() as connection:
  464. try:
  465. model.Model.alembic_autogenerate(connection=connection, message=message)
  466. except CommandError as command_error:
  467. if "Target database is not up to date." not in str(command_error):
  468. raise
  469. console.error(
  470. f"{command_error} Run [bold]reflex db migrate[/bold] to update database."
  471. )
  472. @cli.command()
  473. @loglevel_option
  474. @click.option(
  475. "--app-name",
  476. help="The name of the app to deploy.",
  477. )
  478. @click.option(
  479. "--app-id",
  480. help="The ID of the app to deploy.",
  481. )
  482. @click.option(
  483. "-r",
  484. "--region",
  485. multiple=True,
  486. help="The regions to deploy to. `reflex cloud regions` For multiple envs, repeat this option, e.g. --region sjc --region iad",
  487. )
  488. @click.option(
  489. "--env",
  490. multiple=True,
  491. help="The environment variables to set: <key>=<value>. For multiple envs, repeat this option, e.g. --env k1=v2 --env k2=v2.",
  492. )
  493. @click.option(
  494. "--vmtype",
  495. help="Vm type id. Run `reflex cloud vmtypes` to get options.",
  496. )
  497. @click.option(
  498. "--hostname",
  499. help="The hostname of the frontend.",
  500. )
  501. @click.option(
  502. "--interactive/--no-interactive",
  503. is_flag=True,
  504. default=True,
  505. help="Whether to list configuration options and ask for confirmation.",
  506. )
  507. @click.option(
  508. "--envfile",
  509. help="The path to an env file to use. Will override any envs set manually.",
  510. )
  511. @click.option(
  512. "--project",
  513. help="project id to deploy to",
  514. )
  515. @click.option(
  516. "--project-name",
  517. help="The name of the project to deploy to.",
  518. )
  519. @click.option(
  520. "--token",
  521. help="token to use for auth",
  522. )
  523. @click.option(
  524. "--config-path",
  525. "--config",
  526. help="path to the config file",
  527. )
  528. def deploy(
  529. app_name: str | None,
  530. app_id: str | None,
  531. region: tuple[str, ...],
  532. env: tuple[str],
  533. vmtype: str | None,
  534. hostname: str | None,
  535. interactive: bool,
  536. envfile: str | None,
  537. project: str | None,
  538. project_name: str | None,
  539. token: str | None,
  540. config_path: str | None,
  541. ):
  542. """Deploy the app to the Reflex hosting service."""
  543. from reflex_cli.utils import dependency
  544. from reflex_cli.v2 import cli as hosting_cli
  545. from reflex_cli.v2.deployments import check_version
  546. from reflex.utils import export as export_utils
  547. from reflex.utils import prerequisites
  548. config = get_config()
  549. app_name = app_name or config.app_name
  550. check_version()
  551. environment.REFLEX_COMPILE_CONTEXT.set(constants.CompileContext.DEPLOY)
  552. # Only check requirements if interactive.
  553. # There is user interaction for requirements update.
  554. if interactive:
  555. dependency.check_requirements()
  556. prerequisites.assert_in_reflex_dir()
  557. # Check if we are set up.
  558. if prerequisites.needs_reinit():
  559. _init(name=config.app_name)
  560. prerequisites.check_latest_package_version(constants.ReflexHostingCLI.MODULE_NAME)
  561. hosting_cli.deploy(
  562. app_name=app_name,
  563. app_id=app_id,
  564. export_fn=(
  565. lambda zip_dest_dir,
  566. api_url,
  567. deploy_url,
  568. frontend,
  569. backend,
  570. upload_db,
  571. zipping: export_utils.export(
  572. zip_dest_dir=zip_dest_dir,
  573. api_url=api_url,
  574. deploy_url=deploy_url,
  575. frontend=frontend,
  576. backend=backend,
  577. zipping=zipping,
  578. loglevel=config.loglevel.subprocess_level(),
  579. upload_db_file=upload_db,
  580. )
  581. ),
  582. regions=list(region),
  583. envs=list(env),
  584. vmtype=vmtype,
  585. envfile=envfile,
  586. hostname=hostname,
  587. interactive=interactive,
  588. loglevel=_convert_reflex_loglevel_to_reflex_cli_loglevel(config.loglevel),
  589. token=token,
  590. project=project,
  591. project_name=project_name,
  592. **({"config_path": config_path} if config_path is not None else {}),
  593. )
  594. @cli.command()
  595. @loglevel_option
  596. @click.argument("new_name")
  597. def rename(new_name: str):
  598. """Rename the app in the current directory."""
  599. from reflex.utils import prerequisites
  600. prerequisites.validate_app_name(new_name)
  601. prerequisites.rename_app(new_name, get_config().loglevel)
  602. if TYPE_CHECKING:
  603. from reflex_cli.constants.base import LogLevel as HostingLogLevel
  604. def _convert_reflex_loglevel_to_reflex_cli_loglevel(
  605. loglevel: constants.LogLevel,
  606. ) -> HostingLogLevel:
  607. """Convert a Reflex log level to a Reflex CLI log level.
  608. Args:
  609. loglevel: The Reflex log level to convert.
  610. Returns:
  611. The converted Reflex CLI log level.
  612. """
  613. from reflex_cli.constants.base import LogLevel as HostingLogLevel
  614. if loglevel == constants.LogLevel.DEBUG:
  615. return HostingLogLevel.DEBUG
  616. if loglevel == constants.LogLevel.INFO:
  617. return HostingLogLevel.INFO
  618. if loglevel == constants.LogLevel.WARNING:
  619. return HostingLogLevel.WARNING
  620. if loglevel == constants.LogLevel.ERROR:
  621. return HostingLogLevel.ERROR
  622. if loglevel == constants.LogLevel.CRITICAL:
  623. return HostingLogLevel.CRITICAL
  624. return HostingLogLevel.INFO
  625. if find_spec("typer") and find_spec("typer.main"):
  626. import typer # pyright: ignore[reportMissingImports]
  627. if isinstance(hosting_cli, typer.Typer):
  628. hosting_cli_command = typer.main.get_command(hosting_cli)
  629. else:
  630. hosting_cli_command = hosting_cli
  631. else:
  632. hosting_cli_command = hosting_cli
  633. cli.add_command(hosting_cli_command, name="cloud")
  634. cli.add_command(db_cli, name="db")
  635. cli.add_command(script_cli, name="script")
  636. cli.add_command(custom_components_cli, name="component")
  637. if __name__ == "__main__":
  638. cli()