reflex.py 22 KB

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