splent-cli 1.9.1__tar.gz → 1.10.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (172) hide show
  1. {splent_cli-1.9.1/src/splent_cli.egg-info → splent_cli-1.10.0}/PKG-INFO +2 -1
  2. {splent_cli-1.9.1 → splent_cli-1.10.0}/pyproject.toml +2 -1
  3. {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/check/check_features.py +9 -2
  4. {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/check/check_infra.py +55 -45
  5. {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/coverage.py +7 -4
  6. {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/database/db_console.py +28 -13
  7. {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/database/db_dump.py +25 -10
  8. {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/database/db_migrate.py +24 -4
  9. {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/database/db_reset.py +27 -4
  10. splent_cli-1.10.0/src/splent_cli/commands/database/db_restore.py +117 -0
  11. {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/database/db_rollback.py +44 -3
  12. {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/database/db_seed.py +17 -15
  13. {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/database/db_upgrade.py +8 -0
  14. {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/doctor.py +47 -0
  15. {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/feature/feature_add.py +4 -7
  16. {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/feature/feature_attach.py +4 -9
  17. {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/feature/feature_clean.py +13 -1
  18. {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/feature/feature_clone.py +85 -33
  19. {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/feature/feature_compat.py +2 -3
  20. {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/feature/feature_compile.py +26 -9
  21. {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/feature/feature_contract.py +3 -4
  22. {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/feature/feature_create.py +6 -6
  23. {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/feature/feature_detach.py +31 -10
  24. {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/feature/feature_discard.py +13 -3
  25. {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/feature/feature_env.py +8 -2
  26. {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/feature/feature_fork.py +41 -6
  27. {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/feature/feature_git.py +2 -2
  28. {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/feature/feature_hook_add.py +17 -32
  29. {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/feature/feature_install.py +83 -47
  30. {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/feature/feature_order.py +2 -3
  31. {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/feature/feature_outdated.py +9 -11
  32. {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/feature/feature_pin.py +3 -5
  33. {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/feature/feature_pip_install.py +7 -7
  34. {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/feature/feature_pull.py +6 -2
  35. {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/feature/feature_refine.py +120 -11
  36. {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/feature/feature_release.py +36 -4
  37. {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/feature/feature_remove.py +3 -5
  38. {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/feature/feature_rename.py +81 -37
  39. {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/feature/feature_search.py +23 -1
  40. {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/feature/feature_status.py +32 -4
  41. {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/feature/feature_test.py +33 -1
  42. {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/feature/feature_translate.py +32 -12
  43. {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/feature/feature_unlock.py +82 -18
  44. {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/feature/feature_upgrade.py +17 -5
  45. {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/feature/feature_versions.py +19 -8
  46. {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/feature/feature_xray.py +4 -7
  47. {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/lint.py +16 -6
  48. {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/locust.py +89 -14
  49. {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/product/product_auto_require.py +24 -90
  50. {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/product/product_build.py +15 -11
  51. {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/product/product_clean.py +18 -9
  52. {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/product/product_config.py +4 -1
  53. {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/product/product_configure.py +41 -38
  54. {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/product/product_console.py +4 -1
  55. {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/product/product_create.py +39 -27
  56. {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/product/product_deploy.py +13 -2
  57. {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/product/product_derive.py +3 -3
  58. {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/product/product_deselect.py +2 -2
  59. {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/product/product_env.py +56 -16
  60. {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/product/product_list.py +6 -1
  61. {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/product/product_release.py +48 -35
  62. {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/product/product_resolve.py +66 -3
  63. {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/product/product_restart.py +55 -36
  64. {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/product/product_routes.py +4 -1
  65. {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/product/product_run.py +24 -8
  66. {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/product/product_select.py +9 -2
  67. {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/product/product_signals.py +4 -1
  68. {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/product/product_test.py +4 -3
  69. {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/product/product_up.py +75 -34
  70. {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/selenium.py +16 -12
  71. {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/spl/spl_add_constraints.py +36 -3
  72. {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/spl/spl_add_feature.py +51 -2
  73. {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/uvl/uvl_utils.py +9 -1
  74. {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/services/preflight.py +43 -27
  75. {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/services/release.py +44 -17
  76. splent_cli-1.10.0/src/splent_cli/utils/cache_utils.py +115 -0
  77. {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/utils/command_loader.py +6 -3
  78. {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/utils/contract_freshness.py +10 -2
  79. {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/utils/dynamic_imports.py +26 -12
  80. splent_cli-1.10.0/src/splent_cli/utils/feature_installer.py +86 -0
  81. {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/utils/feature_utils.py +66 -9
  82. {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/utils/git_url.py +18 -3
  83. {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/utils/integrity.py +23 -12
  84. splent_cli-1.10.0/src/splent_cli/utils/io_utils.py +87 -0
  85. {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/utils/manifest.py +12 -2
  86. splent_cli-1.10.0/src/splent_cli/utils/proc.py +109 -0
  87. {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/utils/template_drift.py +12 -5
  88. {splent_cli-1.9.1 → splent_cli-1.10.0/src/splent_cli.egg-info}/PKG-INFO +2 -1
  89. {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli.egg-info/SOURCES.txt +2 -0
  90. {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli.egg-info/requires.txt +1 -0
  91. splent_cli-1.9.1/src/splent_cli/commands/database/db_restore.py +0 -77
  92. splent_cli-1.9.1/src/splent_cli/utils/cache_utils.py +0 -54
  93. splent_cli-1.9.1/src/splent_cli/utils/feature_installer.py +0 -54
  94. {splent_cli-1.9.1 → splent_cli-1.10.0}/LICENSE +0 -0
  95. {splent_cli-1.9.1 → splent_cli-1.10.0}/README.md +0 -0
  96. {splent_cli-1.9.1 → splent_cli-1.10.0}/setup.cfg +0 -0
  97. {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/__init__.py +0 -0
  98. {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/__main__.py +0 -0
  99. {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/cli.py +0 -0
  100. {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/__init__.py +0 -0
  101. {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/cache/__init__.py +0 -0
  102. {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/cache/cache_clear.py +0 -0
  103. {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/cache/cache_orphans.py +0 -0
  104. {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/cache/cache_outdated.py +0 -0
  105. {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/cache/cache_prune.py +0 -0
  106. {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/cache/cache_size.py +0 -0
  107. {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/cache/cache_status.py +0 -0
  108. {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/cache/cache_usage.py +0 -0
  109. {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/cache/cache_versions.py +0 -0
  110. {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/check/__init__.py +0 -0
  111. {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/check/check_deps.py +0 -0
  112. {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/check/check_docker.py +0 -0
  113. {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/check/check_env.py +0 -0
  114. {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/check/check_github.py +0 -0
  115. {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/check/check_product.py +0 -0
  116. {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/check/check_pypi.py +0 -0
  117. {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/check/check_pyproject.py +0 -0
  118. {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/clear/__init__.py +0 -0
  119. {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/clear/clear_build.py +0 -0
  120. {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/clear/clear_log.py +0 -0
  121. {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/clear/clear_uploads.py +0 -0
  122. {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/command/__init__.py +0 -0
  123. {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/command/command_create.py +0 -0
  124. {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/database/db_status.py +0 -0
  125. {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/env/env_list.py +0 -0
  126. {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/env/env_set.py +0 -0
  127. {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/env/env_show.py +0 -0
  128. {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/export/__init__.py +0 -0
  129. {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/export/export_puml.py +0 -0
  130. {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/feature/feature_delete.py +0 -0
  131. {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/feature/feature_drift.py +0 -0
  132. {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/feature/feature_hook_remove.py +0 -0
  133. {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/feature/feature_hooks.py +0 -0
  134. {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/feature/feature_impact.py +0 -0
  135. {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/feature/feature_inject_config.py +0 -0
  136. {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/feature/feature_list.py +0 -0
  137. {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/feature/feature_sync_template.py +0 -0
  138. {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/product/__init__.py +0 -0
  139. {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/product/product_commands.py +0 -0
  140. {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/product/product_containers.py +0 -0
  141. {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/product/product_down.py +0 -0
  142. {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/product/product_drift.py +0 -0
  143. {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/product/product_logs.py +0 -0
  144. {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/product/product_missing.py +0 -0
  145. {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/product/product_port.py +0 -0
  146. {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/product/product_shell.py +0 -0
  147. {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/product/product_sync_template.py +0 -0
  148. {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/product/product_validate.py +0 -0
  149. {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/release/__init__.py +0 -0
  150. {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/release/release_core.py +0 -0
  151. {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/spl/__init__.py +0 -0
  152. {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/spl/spl_configurations.py +0 -0
  153. {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/spl/spl_create.py +0 -0
  154. {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/spl/spl_deps.py +0 -0
  155. {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/spl/spl_features.py +0 -0
  156. {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/spl/spl_fetch.py +0 -0
  157. {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/spl/spl_info.py +0 -0
  158. {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/spl/spl_list.py +0 -0
  159. {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/spl/spl_utils.py +0 -0
  160. {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/tokens_setup.py +0 -0
  161. {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/commands/version.py +0 -0
  162. {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/services/__init__.py +0 -0
  163. {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/services/compose.py +0 -0
  164. {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/services/context.py +0 -0
  165. {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/utils/__init__.py +0 -0
  166. {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/utils/db_utils.py +0 -0
  167. {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/utils/decorators.py +0 -0
  168. {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/utils/lifecycle.py +0 -0
  169. {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli/utils/path_utils.py +0 -0
  170. {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli.egg-info/dependency_links.txt +0 -0
  171. {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli.egg-info/entry_points.txt +0 -0
  172. {splent_cli-1.9.1 → splent_cli-1.10.0}/src/splent_cli.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: splent_cli
3
- Version: 1.9.1
3
+ Version: 1.10.0
4
4
  Summary: SPLENT-CLI is a CLI to be able to work on your development more easily.
5
5
  Author-email: DiversoLab <diversolab@us.es>
6
6
  Project-URL: Homepage, https://github.com/diverso-lab/splent_cli
@@ -11,6 +11,7 @@ License-File: LICENSE
11
11
  Requires-Dist: click==8.1.8
12
12
  Requires-Dist: packaging>=24.0
13
13
  Requires-Dist: requests>=2.31.0
14
+ Requires-Dist: tomli_w>=1.0.0
14
15
  Provides-Extra: dev
15
16
  Requires-Dist: setuptools==80.3.1; extra == "dev"
16
17
  Requires-Dist: pytest==8.3.4; extra == "dev"
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "splent_cli"
7
- version = "1.9.1"
7
+ version = "1.10.0"
8
8
  description = "SPLENT-CLI is a CLI to be able to work on your development more easily."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.13"
@@ -15,6 +15,7 @@ dependencies = [
15
15
  "click==8.1.8",
16
16
  "packaging>=24.0",
17
17
  "requests>=2.31.0",
18
+ "tomli_w>=1.0.0",
18
19
  ]
19
20
 
20
21
  [project.optional-dependencies]
@@ -54,8 +54,15 @@ def check_features():
54
54
  _fail("pyproject.toml not found")
55
55
  raise SystemExit(1)
56
56
 
57
- with open(pyproject_path, "rb") as f:
58
- data = tomllib.load(f)
57
+ try:
58
+ with open(pyproject_path, "rb") as f:
59
+ data = tomllib.load(f)
60
+ except tomllib.TOMLDecodeError as e:
61
+ _fail(f"pyproject.toml is not valid TOML: {e}")
62
+ raise SystemExit(1)
63
+ except OSError as e:
64
+ _fail(f"Could not read pyproject.toml: {e}")
65
+ raise SystemExit(1)
59
66
 
60
67
  env = os.getenv("SPLENT_ENV")
61
68
  features = read_features_from_data(data, env)
@@ -7,18 +7,43 @@ import subprocess
7
7
  import json
8
8
 
9
9
  import click
10
- import tomllib
11
10
 
12
11
  from splent_cli.services import context, compose
13
12
  from splent_cli.utils.feature_utils import read_features_from_data
13
+ from splent_cli.utils.io_utils import load_toml
14
+
15
+
16
+ class _Result:
17
+ """Minimal stand-in for CompletedProcess used by the guarded ``_run``."""
18
+
19
+ __slots__ = ("returncode", "stdout", "stderr")
20
+
21
+ def __init__(self, returncode: int, stdout: str = "", stderr: str = ""):
22
+ self.returncode = returncode
23
+ self.stdout = stdout
24
+ self.stderr = stderr
25
+
26
+
27
+ def _run(cmd: list) -> _Result:
28
+ """Run a docker command, never crashing the diagnostic.
29
+
30
+ Missing tool (FileNotFoundError) and timeouts are mapped to a non-zero
31
+ returncode so callers treat them as a normal FAIL/skip, exactly like
32
+ check_docker._run.
33
+ """
34
+ try:
35
+ r = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
36
+ return _Result(r.returncode, r.stdout, r.stderr)
37
+ except FileNotFoundError:
38
+ return _Result(1, "", "command not found")
39
+ except subprocess.TimeoutExpired:
40
+ return _Result(1, "", "timed out")
14
41
 
15
42
 
16
43
  def _parse_compose_ports(compose_file: str) -> list[tuple[int, str, str]]:
17
44
  """Return [(host_port, service_name, source_label)] from a compose file."""
18
- result = subprocess.run(
19
- ["docker", "compose", "-f", compose_file, "config", "--format", "json"],
20
- capture_output=True,
21
- text=True,
45
+ result = _run(
46
+ ["docker", "compose", "-f", compose_file, "config", "--format", "json"]
22
47
  )
23
48
  if result.returncode != 0:
24
49
  return []
@@ -41,10 +66,8 @@ def _parse_compose_ports(compose_file: str) -> list[tuple[int, str, str]]:
41
66
 
42
67
  def _parse_compose_services(compose_file: str) -> list[tuple[str, str, str]]:
43
68
  """Return [(service_name, container_name_or_None, source_label)]."""
44
- result = subprocess.run(
45
- ["docker", "compose", "-f", compose_file, "config", "--format", "json"],
46
- capture_output=True,
47
- text=True,
69
+ result = _run(
70
+ ["docker", "compose", "-f", compose_file, "config", "--format", "json"]
48
71
  )
49
72
  if result.returncode != 0:
50
73
  return []
@@ -97,8 +120,7 @@ def check_infra():
97
120
  _fail("pyproject.toml not found")
98
121
  raise SystemExit(1)
99
122
 
100
- with open(pyproject_path, "rb") as f:
101
- data = tomllib.load(f)
123
+ data = load_toml(pyproject_path, what="pyproject.toml")
102
124
 
103
125
  env = os.getenv("SPLENT_ENV", "dev")
104
126
  features = read_features_from_data(data, env)
@@ -132,21 +154,19 @@ def check_infra():
132
154
  else:
133
155
  _ok(f"No port conflicts ({len(all_ports)} ports declared)")
134
156
 
135
- # Check against running containers
157
+ # Check against running containers (query docker ps once, not per-port)
136
158
  running_conflicts = []
137
- for port in all_ports:
138
- result = subprocess.run(
139
- ["docker", "ps", "--format", "{{.ID}}\t{{.Names}}\t{{.Ports}}"],
140
- capture_output=True,
141
- text=True,
142
- )
143
- for line in result.stdout.splitlines():
144
- parts = line.split("\t", 2)
145
- if len(parts) < 3:
146
- continue
147
- cid, name, ports_str = parts
148
- if f":{port}->" in ports_str:
149
- running_conflicts.append((port, name))
159
+ ps_result = _run(["docker", "ps", "--format", "{{.ID}}\t{{.Names}}\t{{.Ports}}"])
160
+ if ps_result.returncode == 0:
161
+ ps_lines = ps_result.stdout.splitlines()
162
+ for port in all_ports:
163
+ for line in ps_lines:
164
+ parts = line.split("\t", 2)
165
+ if len(parts) < 3:
166
+ continue
167
+ cid, name, ports_str = parts
168
+ if f":{port}->" in ports_str:
169
+ running_conflicts.append((port, name))
150
170
 
151
171
  if running_conflicts:
152
172
  for port, cname in running_conflicts:
@@ -189,10 +209,8 @@ def check_infra():
189
209
  click.echo(click.style(" Networks", bold=True))
190
210
  required_networks: set[str] = set()
191
211
  for label, cf in compose_files:
192
- result = subprocess.run(
193
- ["docker", "compose", "-f", cf, "config", "--format", "json"],
194
- capture_output=True,
195
- text=True,
212
+ result = _run(
213
+ ["docker", "compose", "-f", cf, "config", "--format", "json"]
196
214
  )
197
215
  if result.returncode != 0:
198
216
  continue
@@ -205,10 +223,8 @@ def check_infra():
205
223
  required_networks.add(net_name)
206
224
 
207
225
  if required_networks:
208
- existing_networks = subprocess.run(
209
- ["docker", "network", "ls", "--format", "{{.Name}}"],
210
- capture_output=True,
211
- text=True,
226
+ existing_networks = _run(
227
+ ["docker", "network", "ls", "--format", "{{.Name}}"]
212
228
  ).stdout.splitlines()
213
229
  for net in sorted(required_networks):
214
230
  if net in existing_networks:
@@ -226,10 +242,8 @@ def check_infra():
226
242
  build_count = 0
227
243
  for label, cf in compose_files:
228
244
  docker_dir = os.path.dirname(cf)
229
- result = subprocess.run(
230
- ["docker", "compose", "-f", cf, "config", "--format", "json"],
231
- capture_output=True,
232
- text=True,
245
+ result = _run(
246
+ ["docker", "compose", "-f", cf, "config", "--format", "json"]
233
247
  )
234
248
  if result.returncode != 0:
235
249
  continue
@@ -268,10 +282,8 @@ def check_infra():
268
282
  services_depended_on: dict[str, str] = {} # depended_svc -> by_svc
269
283
 
270
284
  for label, cf in compose_files:
271
- result = subprocess.run(
272
- ["docker", "compose", "-f", cf, "config", "--format", "json"],
273
- capture_output=True,
274
- text=True,
285
+ result = _run(
286
+ ["docker", "compose", "-f", cf, "config", "--format", "json"]
275
287
  )
276
288
  if result.returncode != 0:
277
289
  continue
@@ -309,10 +321,8 @@ def check_infra():
309
321
  click.echo(click.style(" Volumes", bold=True))
310
322
  all_volumes: dict[str, list[str]] = {} # vol_name -> [labels]
311
323
  for label, cf in compose_files:
312
- result = subprocess.run(
313
- ["docker", "compose", "-f", cf, "config", "--format", "json"],
314
- capture_output=True,
315
- text=True,
324
+ result = _run(
325
+ ["docker", "compose", "-f", cf, "config", "--format", "json"]
316
326
  )
317
327
  if result.returncode != 0:
318
328
  continue
@@ -1,8 +1,8 @@
1
1
  import click
2
- import subprocess
3
2
  import os
4
3
 
5
4
  from splent_cli.utils.path_utils import PathUtils
5
+ from splent_cli.utils.proc import run
6
6
  from splent_cli.services import context
7
7
 
8
8
 
@@ -36,9 +36,12 @@ def coverage(module_name, html):
36
36
  if html:
37
37
  coverage_cmd.extend(["--cov-report", "html"])
38
38
 
39
- try:
40
- subprocess.run(coverage_cmd, check=True)
41
- except subprocess.CalledProcessError:
39
+ result = run(
40
+ coverage_cmd,
41
+ check=False,
42
+ tool_hint="Install it with: pip install pytest pytest-cov",
43
+ )
44
+ if result.returncode != 0:
42
45
  click.echo(
43
46
  click.style("❌ Coverage run failed (tests may be failing).", fg="red")
44
47
  )
@@ -1,9 +1,9 @@
1
1
  import click
2
- import subprocess
3
2
  from dotenv import load_dotenv
4
3
  import os
5
4
 
6
5
  from splent_cli.services import context
6
+ from splent_cli.utils.proc import run, require_tool
7
7
 
8
8
 
9
9
  @click.command(
@@ -38,19 +38,34 @@ def db_console():
38
38
  )
39
39
  raise SystemExit(1)
40
40
 
41
- try:
42
- subprocess.run(
43
- [
44
- "mysql",
45
- f"-h{mariadb_hostname}",
46
- f"-u{mariadb_user}",
47
- f"-p{mariadb_password}",
48
- mariadb_database,
49
- ],
50
- check=True,
41
+ require_tool(
42
+ "mysql",
43
+ "Install the MariaDB/MySQL client (e.g. 'mariadb-client') and ensure it is on your PATH.",
44
+ )
45
+
46
+ result = run(
47
+ [
48
+ "mysql",
49
+ f"-h{mariadb_hostname}",
50
+ f"-u{mariadb_user}",
51
+ f"-p{mariadb_password}",
52
+ mariadb_database,
53
+ ],
54
+ check=False,
55
+ )
56
+ if result.returncode != 0:
57
+ click.secho(
58
+ f"❌ Could not open the MariaDB console (mysql exited {result.returncode}).",
59
+ fg="red",
60
+ )
61
+ click.secho(
62
+ " Is the database container running and reachable?\n"
63
+ f" Tried to connect to host '{mariadb_hostname}' as user "
64
+ f"'{mariadb_user}'.\n"
65
+ " Start it with: splent product:up (or check 'docker ps').",
66
+ fg="yellow",
51
67
  )
52
- except subprocess.CalledProcessError as e:
53
- click.secho(f"❌ Error opening MariaDB console: {e}", fg="red")
68
+ raise SystemExit(1)
54
69
 
55
70
 
56
71
  cli_command = db_console
@@ -1,10 +1,11 @@
1
1
  import click
2
- import subprocess
3
2
  from dotenv import load_dotenv
4
3
  import os
4
+ import tempfile
5
5
  from datetime import datetime
6
6
 
7
7
  from splent_cli.services import context
8
+ from splent_cli.utils.proc import run, require_tool
8
9
 
9
10
 
10
11
  @click.command(
@@ -44,10 +45,23 @@ def db_dump(filename):
44
45
  if not filename.endswith(".sql"):
45
46
  filename += ".sql"
46
47
 
48
+ require_tool(
49
+ "mysqldump",
50
+ "Install the MariaDB/MySQL client tools (provides 'mysqldump').",
51
+ )
52
+
53
+ env = {**os.environ, "MYSQL_PWD": mariadb_password or ""}
54
+
55
+ # Dump to a temp file first and only replace the target on success, so a
56
+ # pre-existing file with the same name is never truncated or deleted if the
57
+ # dump fails midway.
58
+ target_dir = os.path.dirname(os.path.abspath(filename)) or "."
59
+ fd, tmp_path = tempfile.mkstemp(
60
+ prefix=".db_dump_", suffix=".sql.tmp", dir=target_dir
61
+ )
47
62
  try:
48
- env = {**os.environ, "MYSQL_PWD": mariadb_password or ""}
49
- with open(filename, "wb") as out:
50
- subprocess.run(
63
+ with os.fdopen(fd, "wb") as out:
64
+ run(
51
65
  [
52
66
  "mysqldump",
53
67
  f"-h{mariadb_hostname}",
@@ -55,17 +69,18 @@ def db_dump(filename):
55
69
  mariadb_database,
56
70
  ],
57
71
  stdout=out,
58
- check=True,
72
+ text=False,
59
73
  env=env,
74
+ tool_hint="Install the MariaDB/MySQL client tools (provides 'mysqldump').",
60
75
  )
76
+ os.replace(tmp_path, filename)
61
77
  click.echo(
62
78
  click.style(f"Database dump created successfully: {filename}", fg="green")
63
79
  )
64
- except subprocess.CalledProcessError as e:
65
- click.echo(click.style(f"Error creating database dump: {e}", fg="red"))
66
- if os.path.exists(filename):
67
- os.remove(filename)
68
- click.echo(click.style(f"Partial file removed: {filename}", fg="yellow"))
80
+ except BaseException:
81
+ if os.path.exists(tmp_path):
82
+ os.remove(tmp_path)
83
+ raise
69
84
 
70
85
 
71
86
  cli_command = db_dump
@@ -125,19 +125,39 @@ def db_migrate(feature, message, empty):
125
125
  prev_level = alembic_logger.level
126
126
  alembic_logger.setLevel(logging.WARNING)
127
127
 
128
+ gen_error = None
128
129
  try:
129
130
  if empty:
130
131
  alembic_revision(directory=mdir, message=message or feat)
131
132
  else:
132
133
  alembic_migrate(directory=mdir, message=message or feat)
133
134
  except Exception as e:
134
- if os.getenv("SPLENT_DEBUG"):
135
- click.secho(
136
- f" ⚠️ {feat}: migration generation skipped ({e})", fg="yellow"
137
- )
135
+ gen_error = e
138
136
  finally:
139
137
  alembic_logger.setLevel(prev_level)
140
138
 
139
+ # Always surface generation failures — never report "up to date" when the
140
+ # migration actually failed to generate. Show a one-line error (full
141
+ # detail only under SPLENT_DEBUG).
142
+ if gen_error is not None:
143
+ click.echo(
144
+ click.style(
145
+ f" ❌ {feat}: migration generation failed ({gen_error})",
146
+ fg="red",
147
+ )
148
+ )
149
+ if os.getenv("SPLENT_DEBUG"):
150
+ import traceback
151
+
152
+ click.echo(
153
+ "".join(
154
+ traceback.format_exception(
155
+ type(gen_error), gen_error, gen_error.__traceback__
156
+ )
157
+ )
158
+ )
159
+ continue
160
+
141
161
  after = _count_versions(mdir)
142
162
 
143
163
  # A blank migration requested with --empty is intentional: keep it so the
@@ -41,9 +41,14 @@ def db_reset(yes):
41
41
  return
42
42
 
43
43
  # --- STEP 1: Drop ALL tables (including alembic_* and splent_migrations) ---
44
+ # Run every DROP inside a single transaction (engine.begin) so that a
45
+ # mid-operation connection failure rolls back as a unit rather than
46
+ # leaving a half-dropped schema. Note: MySQL/MariaDB issue an implicit
47
+ # COMMIT on DDL, so a full rollback is not guaranteed there; we therefore
48
+ # also surface explicit partial-state recovery guidance on failure.
44
49
  click.echo(click.style("🗑️ Dropping all tables...", fg="yellow"))
45
50
  try:
46
- with db.engine.connect() as conn:
51
+ with db.engine.begin() as conn:
47
52
  conn.execute(text("SET FOREIGN_KEY_CHECKS = 0"))
48
53
  meta = MetaData()
49
54
  meta.reflect(bind=db.engine)
@@ -51,11 +56,21 @@ def db_reset(yes):
51
56
  conn.execute(text(f"DROP TABLE IF EXISTS `{table.name}`"))
52
57
  click.echo(click.style(f" Dropped {table.name}", fg="bright_black"))
53
58
  conn.execute(text("SET FOREIGN_KEY_CHECKS = 1"))
54
- conn.commit()
55
59
  click.echo(click.style("✅ All tables dropped.", fg="yellow"))
56
60
  except Exception as e:
57
61
  click.echo(click.style(f"❌ Error dropping tables: {e}", fg="red"))
58
- return
62
+ click.echo(
63
+ click.style(
64
+ "⚠️ The database may now be in a PARTIAL state: some tables "
65
+ "may have been dropped while others remain.\n"
66
+ " To recover, re-run this command once the database is "
67
+ "reachable again (it is safe to repeat — drops use "
68
+ "DROP TABLE IF EXISTS), or manually drop the remaining "
69
+ "tables / re-create the schema from migrations.",
70
+ fg="red",
71
+ )
72
+ )
73
+ raise SystemExit(1)
59
74
 
60
75
  # --- STEP 2: Recreate splent_migrations tracking table ---
61
76
  click.echo(click.style("📋 Recreating splent_migrations table...", fg="cyan"))
@@ -74,7 +89,15 @@ def db_reset(yes):
74
89
  )
75
90
  except Exception as e:
76
91
  click.echo(click.style(f"❌ Error creating tracking table: {e}", fg="red"))
77
- return
92
+ click.echo(
93
+ click.style(
94
+ "⚠️ All tables were dropped but the tracking table could not "
95
+ "be recreated. Re-run this command once the database is "
96
+ "reachable to finish the reset.",
97
+ fg="red",
98
+ )
99
+ )
100
+ raise SystemExit(1)
78
101
 
79
102
  # --- STEP 3: Clear uploads ---
80
103
  ctx = click.get_current_context()
@@ -0,0 +1,117 @@
1
+ import os
2
+ import click
3
+ from datetime import datetime
4
+ from dotenv import load_dotenv
5
+
6
+ from splent_cli.services import context
7
+ from splent_cli.utils.proc import run
8
+
9
+
10
+ @click.command(
11
+ "db:restore",
12
+ short_help="Restore a MariaDB database from a SQL dump file.",
13
+ )
14
+ @click.argument("filename")
15
+ @click.option("--yes", is_flag=True, help="Skip confirmation prompt.")
16
+ @context.requires_product
17
+ def db_restore(filename, yes):
18
+ """
19
+ Restore the database from FILENAME (a .sql dump).
20
+
21
+ Reads connection credentials from the workspace .env file
22
+ (MARIADB_HOSTNAME, MARIADB_USER, MARIADB_PASSWORD, MARIADB_DATABASE).
23
+
24
+ \b
25
+ Example:
26
+ splent db:restore dump_20250101_120000.sql
27
+ """
28
+ load_dotenv()
29
+
30
+ if not os.path.exists(filename):
31
+ click.secho(f"❌ File not found: {filename}", fg="red")
32
+ raise SystemExit(1)
33
+
34
+ host = os.getenv("MARIADB_HOSTNAME")
35
+ user = os.getenv("MARIADB_USER")
36
+ password = os.getenv("MARIADB_PASSWORD")
37
+ database = os.getenv("MARIADB_DATABASE")
38
+
39
+ missing = [
40
+ k
41
+ for k, v in {
42
+ "MARIADB_HOSTNAME": host,
43
+ "MARIADB_USER": user,
44
+ "MARIADB_PASSWORD": password,
45
+ "MARIADB_DATABASE": database,
46
+ }.items()
47
+ if not v
48
+ ]
49
+
50
+ if missing:
51
+ click.secho(f"❌ Missing env vars: {', '.join(missing)}", fg="red")
52
+ raise SystemExit(1)
53
+
54
+ if not yes:
55
+ click.secho(
56
+ f"⚠️ This will overwrite the database '{database}' with the contents of '{filename}'.",
57
+ fg="yellow",
58
+ )
59
+ if not click.confirm("Continue?"):
60
+ click.echo("❎ Cancelled.")
61
+ raise SystemExit(0)
62
+
63
+ env = {**os.environ, "MYSQL_PWD": password or ""}
64
+
65
+ # Take an automatic pre-restore backup so a corrupt/partial dump cannot
66
+ # leave the live database half-overwritten with no way back.
67
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
68
+ backup_filename = f"pre_restore_{database}_{timestamp}.sql"
69
+ try:
70
+ with open(backup_filename, "wb") as backup_out:
71
+ run(
72
+ ["mysqldump", f"-h{host}", f"-u{user}", database],
73
+ stdout=backup_out,
74
+ env=env,
75
+ tool_hint=(
76
+ "Install the MariaDB/MySQL client tools (provides 'mysqldump'), "
77
+ "e.g. 'mysql-client' / 'mariadb-client'."
78
+ ),
79
+ )
80
+ except click.ClickException:
81
+ # mysqldump missing/failed (often a connection failure): do not proceed
82
+ # to overwrite the live database without a safety net.
83
+ if os.path.exists(backup_filename):
84
+ os.remove(backup_filename)
85
+ click.secho(
86
+ f"❌ Could not create a pre-restore backup of '{database}' "
87
+ f"(check the database is reachable at '{host}' and credentials are correct).\n"
88
+ "Aborting before overwriting the live database.",
89
+ fg="red",
90
+ )
91
+ raise SystemExit(1)
92
+
93
+ click.secho(f"🛟 Pre-restore backup saved to: {backup_filename}", fg="cyan")
94
+
95
+ try:
96
+ with open(filename, "rb") as sql_file:
97
+ run(
98
+ ["mysql", f"-h{host}", f"-u{user}", database],
99
+ stdin=sql_file,
100
+ env=env,
101
+ tool_hint=(
102
+ "Install the MariaDB/MySQL client tools (provides 'mysql'), "
103
+ "e.g. 'mysql-client' / 'mariadb-client'."
104
+ ),
105
+ )
106
+ click.secho(f"✅ Database restored from: {filename}", fg="green")
107
+ except click.ClickException as e:
108
+ click.secho(f"❌ Error restoring database: {e.format_message()}", fg="red")
109
+ click.secho(
110
+ "The database may be in an inconsistent state. To roll back, restore "
111
+ f"the pre-restore backup:\n splent db:restore {backup_filename} --yes",
112
+ fg="yellow",
113
+ )
114
+ raise SystemExit(1)
115
+
116
+
117
+ cli_command = db_restore
@@ -146,7 +146,11 @@ def db_rollback(feature, steps, cascade):
146
146
  click.echo(" ❎ Cancelled.")
147
147
  raise SystemExit(1)
148
148
 
149
- # Rollback dependents first (reverse order)
149
+ # Rollback dependents first (reverse order).
150
+ # NOTE: there is no cross-feature transaction — if a later dependent
151
+ # fails, earlier ones have already been rolled back. Track progress so
152
+ # the failure message can tell the user the partial state to recover.
153
+ rolled_back: list[str] = []
150
154
  for dep in reversed(migrated_dependents):
151
155
  dep_mdir = MigrationManager.get_feature_migration_dir(dep)
152
156
  if dep_mdir:
@@ -160,6 +164,7 @@ def db_rollback(feature, steps, cascade):
160
164
  )
161
165
  MigrationManager.update_feature_status(app, dep, dep_rev)
162
166
  click.secho(f" ✅ {dep} → {dep_rev or 'base'}", fg="green")
167
+ rolled_back.append(dep)
163
168
 
164
169
  for entry in get_features_from_pyproject() or []:
165
170
  key, ns, name, version = resolve_feature_key_from_entry(entry)
@@ -177,6 +182,22 @@ def db_rollback(feature, steps, cascade):
177
182
  break
178
183
  except Exception as e:
179
184
  click.secho(f" ❌ {dep}: {e}", fg="red")
185
+ click.secho(
186
+ f" ⚠️ Partial rollback: '{feature}' was NOT rolled back.",
187
+ fg="yellow",
188
+ )
189
+ if rolled_back:
190
+ click.secho(
191
+ " Already rolled back (not restored): "
192
+ + ", ".join(rolled_back),
193
+ fg="yellow",
194
+ )
195
+ click.secho(
196
+ " To recover, re-apply migrations with "
197
+ "'flask db:migrate' for those features, "
198
+ "or fix the error above and re-run db:rollback.",
199
+ fg="yellow",
200
+ )
180
201
  raise SystemExit(1)
181
202
 
182
203
  # Rollback the target feature
@@ -206,7 +227,19 @@ def db_rollback(feature, steps, cascade):
206
227
  )
207
228
  break
208
229
  except Exception as e:
209
- click.secho(f" {feature}: {e}", fg="red")
230
+ click.secho(f" {feature}: {e}", fg="red")
231
+ if migrated_dependents:
232
+ click.secho(
233
+ " ⚠️ Partial rollback: dependent feature(s) were already "
234
+ "rolled back but '" + feature + "' failed.",
235
+ fg="yellow",
236
+ )
237
+ click.secho(
238
+ " The product is in a mixed state. Fix the error above and "
239
+ "re-run db:rollback, or re-apply migrations with 'flask "
240
+ "db:migrate' to restore the rolled-back dependents.",
241
+ fg="yellow",
242
+ )
210
243
  raise SystemExit(1)
211
244
 
212
245
  # Offer to delete migration files if fully rolled back to base
@@ -218,8 +251,16 @@ def db_rollback(feature, steps, cascade):
218
251
  for f in os.listdir(versions_dir)
219
252
  if f.endswith(".py") and not f.startswith("__")
220
253
  ]
254
+ if migration_files:
255
+ click.secho(
256
+ " ⚠️ Deleting migration files is IRREVERSIBLE — the "
257
+ "files cannot be recovered and the feature will need new "
258
+ "migrations to be re-applied.",
259
+ fg="yellow",
260
+ )
221
261
  if migration_files and click.confirm(
222
- f" Delete {len(migration_files)} migration file(s)?",
262
+ f" Permanently delete {len(migration_files)} migration "
263
+ "file(s)? (irreversible)",
223
264
  default=False,
224
265
  ):
225
266
  for f in migration_files: