reflex.py 22 KB

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