splent-cli 1.11.0__tar.gz → 1.13.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.11.0/src/splent_cli.egg-info → splent_cli-1.13.0}/PKG-INFO +1 -1
  2. {splent_cli-1.11.0 → splent_cli-1.13.0}/pyproject.toml +1 -1
  3. splent_cli-1.13.0/src/splent_cli/commands/database/db_reset.py +267 -0
  4. {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/feature/feature_unlock.py +10 -2
  5. splent_cli-1.13.0/src/splent_cli/commands/locust.py +140 -0
  6. {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/spl/spl_add_feature.py +33 -36
  7. splent_cli-1.13.0/src/splent_cli/commands/spl/spl_compare.py +88 -0
  8. {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/spl/spl_create.py +4 -1
  9. {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/utils/dynamic_imports.py +10 -0
  10. {splent_cli-1.11.0 → splent_cli-1.13.0/src/splent_cli.egg-info}/PKG-INFO +1 -1
  11. {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli.egg-info/SOURCES.txt +1 -0
  12. splent_cli-1.11.0/src/splent_cli/commands/database/db_reset.py +0 -159
  13. splent_cli-1.11.0/src/splent_cli/commands/locust.py +0 -271
  14. {splent_cli-1.11.0 → splent_cli-1.13.0}/LICENSE +0 -0
  15. {splent_cli-1.11.0 → splent_cli-1.13.0}/README.md +0 -0
  16. {splent_cli-1.11.0 → splent_cli-1.13.0}/setup.cfg +0 -0
  17. {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/__init__.py +0 -0
  18. {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/__main__.py +0 -0
  19. {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/cli.py +0 -0
  20. {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/__init__.py +0 -0
  21. {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/cache/__init__.py +0 -0
  22. {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/cache/cache_clear.py +0 -0
  23. {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/cache/cache_orphans.py +0 -0
  24. {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/cache/cache_outdated.py +0 -0
  25. {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/cache/cache_prune.py +0 -0
  26. {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/cache/cache_size.py +0 -0
  27. {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/cache/cache_status.py +0 -0
  28. {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/cache/cache_usage.py +0 -0
  29. {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/cache/cache_versions.py +0 -0
  30. {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/check/__init__.py +0 -0
  31. {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/check/check_deps.py +0 -0
  32. {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/check/check_docker.py +0 -0
  33. {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/check/check_env.py +0 -0
  34. {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/check/check_features.py +0 -0
  35. {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/check/check_github.py +0 -0
  36. {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/check/check_infra.py +0 -0
  37. {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/check/check_product.py +0 -0
  38. {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/check/check_pypi.py +0 -0
  39. {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/check/check_pyproject.py +0 -0
  40. {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/clear/__init__.py +0 -0
  41. {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/clear/clear_build.py +0 -0
  42. {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/clear/clear_log.py +0 -0
  43. {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/clear/clear_uploads.py +0 -0
  44. {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/command/__init__.py +0 -0
  45. {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/command/command_create.py +0 -0
  46. {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/coverage.py +0 -0
  47. {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/database/db_console.py +0 -0
  48. {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/database/db_dump.py +0 -0
  49. {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/database/db_migrate.py +0 -0
  50. {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/database/db_restore.py +0 -0
  51. {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/database/db_rollback.py +0 -0
  52. {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/database/db_seed.py +0 -0
  53. {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/database/db_status.py +0 -0
  54. {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/database/db_upgrade.py +0 -0
  55. {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/doctor.py +0 -0
  56. {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/env/env_list.py +0 -0
  57. {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/env/env_set.py +0 -0
  58. {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/env/env_show.py +0 -0
  59. {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/export/__init__.py +0 -0
  60. {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/export/export_puml.py +0 -0
  61. {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/feature/feature_add.py +0 -0
  62. {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/feature/feature_attach.py +0 -0
  63. {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/feature/feature_clean.py +0 -0
  64. {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/feature/feature_clone.py +0 -0
  65. {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/feature/feature_compat.py +0 -0
  66. {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/feature/feature_compile.py +0 -0
  67. {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/feature/feature_contract.py +0 -0
  68. {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/feature/feature_create.py +0 -0
  69. {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/feature/feature_delete.py +0 -0
  70. {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/feature/feature_detach.py +0 -0
  71. {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/feature/feature_discard.py +0 -0
  72. {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/feature/feature_drift.py +0 -0
  73. {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/feature/feature_env.py +0 -0
  74. {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/feature/feature_fork.py +0 -0
  75. {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/feature/feature_git.py +0 -0
  76. {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/feature/feature_hook_add.py +0 -0
  77. {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/feature/feature_hook_remove.py +0 -0
  78. {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/feature/feature_hooks.py +0 -0
  79. {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/feature/feature_impact.py +0 -0
  80. {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/feature/feature_inject_config.py +0 -0
  81. {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/feature/feature_install.py +0 -0
  82. {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/feature/feature_list.py +0 -0
  83. {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/feature/feature_order.py +0 -0
  84. {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/feature/feature_outdated.py +0 -0
  85. {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/feature/feature_pin.py +0 -0
  86. {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/feature/feature_pip_install.py +0 -0
  87. {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/feature/feature_pull.py +0 -0
  88. {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/feature/feature_refine.py +0 -0
  89. {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/feature/feature_release.py +0 -0
  90. {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/feature/feature_remove.py +0 -0
  91. {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/feature/feature_rename.py +0 -0
  92. {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/feature/feature_search.py +0 -0
  93. {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/feature/feature_status.py +0 -0
  94. {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/feature/feature_sync_template.py +0 -0
  95. {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/feature/feature_test.py +0 -0
  96. {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/feature/feature_translate.py +0 -0
  97. {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/feature/feature_upgrade.py +0 -0
  98. {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/feature/feature_versions.py +0 -0
  99. {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/feature/feature_xray.py +0 -0
  100. {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/lint.py +0 -0
  101. {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/product/__init__.py +0 -0
  102. {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/product/product_auto_require.py +0 -0
  103. {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/product/product_build.py +0 -0
  104. {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/product/product_clean.py +0 -0
  105. {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/product/product_commands.py +0 -0
  106. {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/product/product_config.py +0 -0
  107. {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/product/product_configure.py +0 -0
  108. {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/product/product_console.py +0 -0
  109. {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/product/product_containers.py +0 -0
  110. {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/product/product_create.py +0 -0
  111. {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/product/product_deploy.py +0 -0
  112. {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/product/product_derive.py +0 -0
  113. {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/product/product_deselect.py +0 -0
  114. {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/product/product_down.py +0 -0
  115. {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/product/product_drift.py +0 -0
  116. {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/product/product_env.py +0 -0
  117. {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/product/product_list.py +0 -0
  118. {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/product/product_logs.py +0 -0
  119. {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/product/product_missing.py +0 -0
  120. {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/product/product_port.py +0 -0
  121. {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/product/product_release.py +0 -0
  122. {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/product/product_resolve.py +0 -0
  123. {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/product/product_restart.py +0 -0
  124. {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/product/product_routes.py +0 -0
  125. {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/product/product_run.py +0 -0
  126. {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/product/product_select.py +0 -0
  127. {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/product/product_shell.py +0 -0
  128. {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/product/product_signals.py +0 -0
  129. {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/product/product_sync_template.py +0 -0
  130. {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/product/product_test.py +0 -0
  131. {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/product/product_up.py +0 -0
  132. {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/product/product_validate.py +0 -0
  133. {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/release/__init__.py +0 -0
  134. {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/release/release_core.py +0 -0
  135. {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/selenium.py +0 -0
  136. {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/spl/__init__.py +0 -0
  137. {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/spl/spl_add_constraints.py +0 -0
  138. {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/spl/spl_configurations.py +0 -0
  139. {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/spl/spl_deps.py +0 -0
  140. {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/spl/spl_features.py +0 -0
  141. {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/spl/spl_fetch.py +0 -0
  142. {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/spl/spl_info.py +0 -0
  143. {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/spl/spl_list.py +0 -0
  144. {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/spl/spl_utils.py +0 -0
  145. {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/tokens_setup.py +0 -0
  146. {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/uvl/uvl_utils.py +0 -0
  147. {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/commands/version.py +0 -0
  148. {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/services/__init__.py +0 -0
  149. {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/services/compose.py +0 -0
  150. {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/services/context.py +0 -0
  151. {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/services/preflight.py +0 -0
  152. {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/services/release.py +0 -0
  153. {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/utils/__init__.py +0 -0
  154. {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/utils/cache_utils.py +0 -0
  155. {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/utils/command_loader.py +0 -0
  156. {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/utils/contract_freshness.py +0 -0
  157. {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/utils/db_utils.py +0 -0
  158. {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/utils/decorators.py +0 -0
  159. {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/utils/feature_installer.py +0 -0
  160. {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/utils/feature_utils.py +0 -0
  161. {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/utils/git_url.py +0 -0
  162. {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/utils/integrity.py +0 -0
  163. {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/utils/io_utils.py +0 -0
  164. {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/utils/lifecycle.py +0 -0
  165. {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/utils/manifest.py +0 -0
  166. {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/utils/path_utils.py +0 -0
  167. {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/utils/proc.py +0 -0
  168. {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli/utils/template_drift.py +0 -0
  169. {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli.egg-info/dependency_links.txt +0 -0
  170. {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli.egg-info/entry_points.txt +0 -0
  171. {splent_cli-1.11.0 → splent_cli-1.13.0}/src/splent_cli.egg-info/requires.txt +0 -0
  172. {splent_cli-1.11.0 → splent_cli-1.13.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.11.0
3
+ Version: 1.13.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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "splent_cli"
7
- version = "1.11.0"
7
+ version = "1.13.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"
@@ -0,0 +1,267 @@
1
+ import glob
2
+ import importlib
3
+ import os
4
+ import tomllib
5
+
6
+ import click
7
+ from flask import current_app
8
+ from flask_migrate import upgrade as alembic_upgrade
9
+ from sqlalchemy import text, MetaData
10
+
11
+ from splent_cli.utils.decorators import requires_db
12
+ from splent_cli.services import context
13
+ from splent_cli.utils.lifecycle import advance_state, resolve_feature_key_from_entry
14
+ from splent_framework.db import db
15
+ from splent_framework.managers.migration_manager import (
16
+ MigrationManager,
17
+ SPLENT_MIGRATIONS_TABLE,
18
+ alembic_version_table,
19
+ )
20
+ from splent_framework.utils.feature_utils import get_features_from_pyproject
21
+ from splent_framework.utils.path_utils import PathUtils
22
+ from splent_cli.commands.clear.clear_uploads import clear_uploads
23
+
24
+
25
+ # =====================================================================
26
+ # Helpers for the per-feature reset
27
+ # =====================================================================
28
+ def _normalize(feature_name: str) -> str:
29
+ """Accept 'projects' or 'splent_feature_projects' -> full package name."""
30
+ short = feature_name.split("/")[-1].replace("splent_feature_", "")
31
+ return f"splent_feature_{short}"
32
+
33
+
34
+ def _tables_owned_by(feature_full: str) -> list[str]:
35
+ """Tables whose model classes live in the given feature's package."""
36
+ import_name = f"splent_io.{feature_full}"
37
+ try:
38
+ importlib.import_module(f"{import_name}.models")
39
+ except Exception:
40
+ pass
41
+ tables = []
42
+ for mapper in db.Model.registry.mappers:
43
+ cls = mapper.class_
44
+ if getattr(cls, "__module__", "").startswith(import_name):
45
+ tables.append(cls.__table__.name)
46
+ return sorted(set(tables))
47
+
48
+
49
+ def _refiners_of(feature_full: str, product_path: str) -> list[str]:
50
+ """Installed features whose [tool.splent.refinement].refines == feature."""
51
+ refiners = []
52
+ pattern = os.path.join(product_path, "features", "*", "*", "pyproject.toml")
53
+ for pp in glob.glob(pattern):
54
+ try:
55
+ with open(pp, "rb") as f:
56
+ data = tomllib.load(f)
57
+ except Exception:
58
+ continue
59
+ ref = data.get("tool", {}).get("splent", {}).get("refinement", {})
60
+ if ref.get("refines") == feature_full:
61
+ name = data.get("project", {}).get("name", "")
62
+ if name:
63
+ refiners.append(name)
64
+ return refiners
65
+
66
+
67
+ def _reapply(app, feats: list[str], entry_lookup: dict, product_path, product_name):
68
+ dirs = MigrationManager.get_all_feature_migration_dirs()
69
+ for feat in feats:
70
+ mdir = dirs.get(feat)
71
+ if not mdir:
72
+ continue
73
+ try:
74
+ alembic_upgrade(directory=mdir)
75
+ revision = MigrationManager.get_current_feature_revision(feat, db.engine)
76
+ MigrationManager.update_feature_status(app, feat, revision)
77
+ click.echo(click.style(f" ✅ {feat} → {revision or 'head'}", fg="green"))
78
+ info = entry_lookup.get(feat)
79
+ if info:
80
+ key, ns, name, version = info
81
+ advance_state(
82
+ product_path,
83
+ product_name,
84
+ key,
85
+ to="migrated",
86
+ namespace=ns,
87
+ name=name,
88
+ version=version,
89
+ )
90
+ except ImportError as e:
91
+ if "models" in str(e):
92
+ continue
93
+ click.echo(click.style(f" ❌ {feat}: {e}", fg="red"))
94
+ except Exception as e:
95
+ click.echo(click.style(f" ❌ {feat}: {e}", fg="red"))
96
+
97
+
98
+ def _entry_lookup() -> dict:
99
+ lookup = {}
100
+ for entry in get_features_from_pyproject() or []:
101
+ key, ns, name, version = resolve_feature_key_from_entry(entry)
102
+ lookup[name] = (key, ns, name, version)
103
+ return lookup
104
+
105
+
106
+ # =====================================================================
107
+ # Command
108
+ # =====================================================================
109
+ @requires_db
110
+ @click.command(
111
+ "db:reset",
112
+ short_help="Drop all tables (or just one feature's) and re-apply migrations.",
113
+ )
114
+ @click.argument("feature_name", required=False)
115
+ @click.option("-y", "--yes", is_flag=True, help="Confirm without prompting.")
116
+ @context.requires_product
117
+ def db_reset(feature_name, yes):
118
+ """Reset the database.
119
+
120
+ With no argument: drops ALL tables and re-applies every feature migration.
121
+
122
+ With FEATURE_NAME (e.g. ``db:reset projects``): drops only the tables that
123
+ feature owns, clears their migration tracking, and re-applies that
124
+ feature's migrations plus any feature that refines it — leaving every other
125
+ feature's data untouched. Useful after changing a model/refinement.
126
+ """
127
+ app = current_app
128
+ product_path = PathUtils.get_app_base_dir()
129
+ product_name = os.getenv("SPLENT_APP", "")
130
+
131
+ if feature_name:
132
+ _reset_one_feature(app, feature_name, yes, product_path, product_name)
133
+ return
134
+
135
+ _reset_everything(app, yes, product_path, product_name)
136
+
137
+
138
+ def _reset_one_feature(app, feature_name, yes, product_path, product_name):
139
+ feature_full = _normalize(feature_name)
140
+ tables = _tables_owned_by(feature_full)
141
+ refiners = _refiners_of(feature_full, product_path)
142
+ feats = [feature_full] + refiners
143
+
144
+ if not tables:
145
+ click.secho(
146
+ f"⚠️ '{feature_full}' owns no tables (nothing to drop). "
147
+ f"If it is a refinement, reset the base feature instead.",
148
+ fg="yellow",
149
+ )
150
+ return
151
+
152
+ click.echo()
153
+ click.secho(f"Reset feature: {feature_full}", fg="cyan", bold=True)
154
+ click.echo(f" tables to drop : {', '.join(tables)}")
155
+ if refiners:
156
+ click.echo(f" also re-applies: {', '.join(refiners)} (refiners)")
157
+ if not yes and not click.confirm(
158
+ "⚠️ This drops those tables and re-applies their migrations. Continue?",
159
+ abort=True,
160
+ ):
161
+ return
162
+
163
+ # Drop the feature's tables
164
+ with db.engine.begin() as conn:
165
+ conn.execute(text("SET FOREIGN_KEY_CHECKS = 0"))
166
+ for t in tables:
167
+ conn.execute(text(f"DROP TABLE IF EXISTS `{t}`"))
168
+ click.echo(click.style(f" Dropped {t}", fg="bright_black"))
169
+ conn.execute(text("SET FOREIGN_KEY_CHECKS = 1"))
170
+
171
+ # Clear migration tracking for the feature and its refiners — both the
172
+ # central splent_migrations row AND each feature's alembic version table —
173
+ # so their migrations are re-applied from scratch onto the fresh tables.
174
+ with db.engine.begin() as conn:
175
+ conn.execute(text("SET FOREIGN_KEY_CHECKS = 0"))
176
+ for feat in feats:
177
+ conn.execute(
178
+ text(f"DELETE FROM `{SPLENT_MIGRATIONS_TABLE}` WHERE feature = :f"),
179
+ {"f": feat},
180
+ )
181
+ conn.execute(text(f"DROP TABLE IF EXISTS `{alembic_version_table(feat)}`"))
182
+ conn.execute(text("SET FOREIGN_KEY_CHECKS = 1"))
183
+
184
+ click.secho("⬆️ Re-applying migrations...", fg="cyan")
185
+ _reapply(app, feats, _entry_lookup(), product_path, product_name)
186
+ click.secho(f"\n🎉 Feature '{feature_full}' reset complete.", fg="green")
187
+
188
+
189
+ def _reset_everything(app, yes, product_path, product_name):
190
+ if not yes and not click.confirm(
191
+ "⚠️ WARNING: This will DROP all tables and clear uploads. Are you sure?",
192
+ abort=True,
193
+ ):
194
+ return
195
+
196
+ click.echo(click.style("🗑️ Dropping all tables...", fg="yellow"))
197
+ try:
198
+ with db.engine.begin() as conn:
199
+ conn.execute(text("SET FOREIGN_KEY_CHECKS = 0"))
200
+ meta = MetaData()
201
+ meta.reflect(bind=db.engine)
202
+ for table in meta.sorted_tables:
203
+ conn.execute(text(f"DROP TABLE IF EXISTS `{table.name}`"))
204
+ click.echo(click.style(f" Dropped {table.name}", fg="bright_black"))
205
+ conn.execute(text("SET FOREIGN_KEY_CHECKS = 1"))
206
+ click.echo(click.style("✅ All tables dropped.", fg="yellow"))
207
+ except Exception as e:
208
+ click.echo(click.style(f"❌ Error dropping tables: {e}", fg="red"))
209
+ click.echo(
210
+ click.style(
211
+ "⚠️ The database may now be in a PARTIAL state: some tables "
212
+ "may have been dropped while others remain.\n"
213
+ " To recover, re-run this command once the database is "
214
+ "reachable again (it is safe to repeat — drops use "
215
+ "DROP TABLE IF EXISTS), or manually drop the remaining "
216
+ "tables / re-create the schema from migrations.",
217
+ fg="red",
218
+ )
219
+ )
220
+ raise SystemExit(1)
221
+
222
+ click.echo(click.style("📋 Recreating splent_migrations table...", fg="cyan"))
223
+ try:
224
+ with db.engine.begin() as conn:
225
+ conn.execute(
226
+ text(
227
+ f"""
228
+ CREATE TABLE IF NOT EXISTS `{SPLENT_MIGRATIONS_TABLE}` (
229
+ `feature` VARCHAR(255) NOT NULL,
230
+ `last_migration` VARCHAR(255) DEFAULT NULL,
231
+ PRIMARY KEY (`feature`)
232
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
233
+ """
234
+ )
235
+ )
236
+ except Exception as exc:
237
+ # The drop above succeeded, so the schema is empty but the migration
238
+ # tracking table is gone: surface the partial state instead of a raw
239
+ # traceback, and tell the operator how to converge.
240
+ click.echo(
241
+ click.style(
242
+ f"Failed to recreate the migration tracking table `{SPLENT_MIGRATIONS_TABLE}`: {exc}\n"
243
+ "The database is empty but untracked. Re-run 'splent db:reset' once the "
244
+ "database connection is back.",
245
+ fg="red",
246
+ )
247
+ )
248
+ raise SystemExit(1)
249
+
250
+ ctx = click.get_current_context()
251
+ ctx.invoke(clear_uploads)
252
+
253
+ dirs = MigrationManager.get_all_feature_migration_dirs()
254
+ if not dirs:
255
+ click.echo(click.style("⚠️ No feature migrations found.", fg="yellow"))
256
+ else:
257
+ click.echo(
258
+ click.style(
259
+ f"⬆️ Applying migrations for {len(dirs)} features...", fg="cyan"
260
+ )
261
+ )
262
+ _reapply(app, list(dirs.keys()), _entry_lookup(), product_path, product_name)
263
+
264
+ click.echo(click.style("\n🎉 Database reset complete.", fg="green"))
265
+
266
+
267
+ cli_command = db_reset
@@ -4,7 +4,6 @@ import tomllib
4
4
  import click
5
5
  import requests
6
6
  from splent_cli.services import context, compose
7
- from splent_cli.utils.feature_utils import read_features_from_data
8
7
  from splent_cli.utils.cache_utils import make_feature_writable
9
8
  from splent_cli.utils.proc import run
10
9
  from splent_cli.utils.io_utils import load_toml, atomic_write
@@ -343,7 +342,16 @@ def feature_edit(feature_name, edit_all, force):
343
342
  with open(pyproject_path, "rb") as f:
344
343
  data = tomllib.load(f)
345
344
 
346
- features = read_features_from_data(data)
345
+ # Look across all feature lists (features, features_dev, features_prod) so
346
+ # that dev/prod-only features (e.g. admin) can also be unlocked, not just
347
+ # the default `features` list.
348
+ splent_cfg = data.get("tool", {}).get("splent", {})
349
+ features = [
350
+ entry
351
+ for key in ("features", "features_dev", "features_prod")
352
+ for entry in (splent_cfg.get(key) or [])
353
+ if isinstance(entry, str)
354
+ ]
347
355
 
348
356
  # ── Single feature ────────────────────────────────────────────────
349
357
  if feature_name:
@@ -0,0 +1,140 @@
1
+ """splent locust — load testing for the active product.
2
+
3
+ Locust runs inside the product's web container, which already has
4
+ splent_framework and every installed feature importable, so no extra image is
5
+ built. Discovery is done by splent_framework's locustfile bootstrap (1.7.1+),
6
+ which finds each feature's locustfile on its own; a feature argument narrows it
7
+ through SPLENT_LOCUSTFILES.
8
+
9
+ The web interface listens on port 8089 inside the container. Products
10
+ scaffolded by this CLI publish that port; for an older product add
11
+ "8089:8089" to the web service's ports and recreate the container.
12
+ """
13
+
14
+ import os
15
+ import shlex
16
+
17
+ import click
18
+
19
+ from splent_cli.services import context
20
+ from splent_cli.utils.proc import require_docker, run
21
+
22
+ # Feature locustfile locations, relative to the workspace. Kept in sync with
23
+ # splent_framework.bootstraps.locustfile_bootstrap.DEFAULT_PATTERNS.
24
+ FEATURE_LOCUSTFILE_PATTERNS = (
25
+ "features/*/{feature}/tests/load/locustfile.py",
26
+ "features/*/{feature}/tests/locustfile.py",
27
+ "features/*/{feature}/src/*/*/tests/load/locustfile.py",
28
+ "app/features/{feature}/tests/locustfile.py",
29
+ )
30
+
31
+ # Resolved inside the container: the framework bootstrap that discovers and
32
+ # re-exports every feature's HttpUser classes.
33
+ BOOTSTRAP_SNIPPET = (
34
+ "locust -f \"$(python -c 'import splent_framework.bootstraps.locustfile_bootstrap as m; "
35
+ "print(m.__file__)')\" --web-host 0.0.0.0"
36
+ )
37
+
38
+
39
+ def _web_container() -> str:
40
+ return f"{context.require_app()}_web"
41
+
42
+
43
+ def _container_running(name: str) -> bool:
44
+ result = run(
45
+ ["docker", "ps", "-q", "-f", f"name=^{name}$"], check=False, capture=True
46
+ )
47
+ return result.returncode == 0 and bool(result.stdout.strip())
48
+
49
+
50
+ def _locust_installed(name: str) -> bool:
51
+ result = run(
52
+ ["docker", "exec", name, "python", "-c", "import locust"],
53
+ check=False,
54
+ capture=True,
55
+ )
56
+ return result.returncode == 0
57
+
58
+
59
+ def _feature_patterns(feature: str) -> str:
60
+ return ",".join(p.format(feature=feature) for p in FEATURE_LOCUSTFILE_PATTERNS)
61
+
62
+
63
+ def _feature_has_locustfile(workspace: str, feature: str) -> bool:
64
+ import glob
65
+
66
+ for pattern in FEATURE_LOCUSTFILE_PATTERNS:
67
+ if glob.glob(os.path.join(workspace, pattern.format(feature=feature))):
68
+ return True
69
+ return False
70
+
71
+
72
+ @click.command(
73
+ "locust", short_help="Run Locust load tests inside the product web container."
74
+ )
75
+ @click.argument("feature", required=False)
76
+ @context.requires_product
77
+ def locust(feature):
78
+ require_docker()
79
+ container = _web_container()
80
+
81
+ if not _container_running(container):
82
+ raise click.ClickException(
83
+ f"Container '{container}' is not running. Start the product first: splent product:up"
84
+ )
85
+
86
+ if not _locust_installed(container):
87
+ raise click.ClickException(
88
+ f"locust is not installed in '{container}'.\n"
89
+ 'Add "locust" to [project.optional-dependencies].dev in the product\'s '
90
+ "pyproject.toml and rebuild (splent product:derive), or install it once "
91
+ f"with: docker exec {container} pip install locust"
92
+ )
93
+
94
+ workspace = str(context.workspace())
95
+ env_args = ["-e", "WORKING_DIR=/workspace"]
96
+ if feature:
97
+ if not _feature_has_locustfile(workspace, feature):
98
+ raise click.ClickException(
99
+ f"No locustfile found for feature '{feature}'. Looked under: "
100
+ + ", ".join(FEATURE_LOCUSTFILE_PATTERNS)
101
+ )
102
+ env_args += ["-e", f"SPLENT_LOCUSTFILES={_feature_patterns(feature)}"]
103
+
104
+ check = run(
105
+ ["docker", "exec", container, "sh", "-c", "pgrep -f 'locust' >/dev/null"],
106
+ check=False,
107
+ capture=True,
108
+ )
109
+ if check.returncode == 0:
110
+ click.echo("Locust is already running in the product container.")
111
+ return
112
+
113
+ cmd = ["docker", "exec", "-d", *env_args, container, "sh", "-c", BOOTSTRAP_SNIPPET]
114
+ click.echo(f"Command: {' '.join(shlex.quote(c) for c in cmd)}")
115
+ run(cmd)
116
+ click.echo(click.style("Locust is running at http://localhost:8089", fg="green"))
117
+ click.echo("Stop it with: splent locust:stop")
118
+
119
+
120
+ @click.command(
121
+ "locust:stop", short_help="Stop the Locust process in the product web container."
122
+ )
123
+ @context.requires_product
124
+ def locust_stop():
125
+ require_docker()
126
+ container = _web_container()
127
+
128
+ if not _container_running(container):
129
+ click.echo(f"Container '{container}' is not running; nothing to stop.")
130
+ return
131
+
132
+ result = run(
133
+ ["docker", "exec", container, "sh", "-c", "pkill -f 'locust' || true"],
134
+ check=False,
135
+ capture=True,
136
+ )
137
+ if result.returncode == 0:
138
+ click.echo("Locust stopped.")
139
+ else:
140
+ click.echo("No Locust process was running.")
@@ -219,45 +219,42 @@ def spl_add_feature(spl_name, feature_package, org):
219
219
  with open(uvl_path, "r", encoding="utf-8") as f:
220
220
  lines = f.read().splitlines()
221
221
 
222
- # Find the line before "constraints" to insert the feature
223
- insert_idx = None
224
- constraints_idx = None
225
- for i, line in enumerate(lines):
226
- if line.strip() == "constraints":
227
- constraints_idx = i
228
- insert_idx = i
229
- break
230
-
231
- if insert_idx is None:
232
- insert_idx = len(lines)
233
-
234
- # The line-insertion below assumes "constraints" lives on its own line.
235
- # If it doesn't (e.g. malformed or unexpected layout), bail out before
236
- # touching the file rather than silently corrupting the model.
237
- if detected_deps and constraints_idx is None:
238
- click.secho(
239
- f" ❌ No 'constraints' section found in {uvl_path}.\n"
240
- f" Refusing to edit: the UVL layout is not what this command "
241
- f"expects (a 'constraints' header on its own line). "
242
- f"Add the feature and constraints manually.",
243
- fg="red",
244
- )
245
- raise SystemExit(1)
246
-
247
- # Insert feature declaration
248
- feature_line = (
249
- f"\t\toptional\n\t\t\t{short_name} {{org '{org}', package '{feature_package}'}}"
222
+ # Locate the (optional) "constraints" header and an existing "optional" group.
223
+ # A fresh SPL has neither. UVL rejects an EMPTY "constraints" section, so we
224
+ # only create that section when there is at least one constraint to add.
225
+ constraints_idx = next(
226
+ (i for i, line in enumerate(lines) if line.strip() == "constraints"), None
250
227
  )
251
- lines.insert(insert_idx, feature_line)
228
+ optional_idx = next(
229
+ (i for i, line in enumerate(lines) if line.strip() == "optional"), None
230
+ )
231
+
232
+ # Insert the feature under a SINGLE shared "optional" group, reusing it if it
233
+ # already exists (so repeated calls don't emit multiple 'optional' headers,
234
+ # which produced invalid UVL). The feature goes just before "constraints" if
235
+ # present, otherwise at the end of the features block. Indentation is tabs
236
+ # throughout, consistent with spl:create.
237
+ feat_at = constraints_idx if constraints_idx is not None else len(lines)
238
+ feature_decl = f"\t\t\t{short_name} {{org '{org}', package '{feature_package}'}}"
239
+ if optional_idx is not None:
240
+ lines.insert(feat_at, feature_decl)
241
+ if constraints_idx is not None:
242
+ constraints_idx += 1
243
+ else:
244
+ lines.insert(feat_at, "\t\toptional")
245
+ lines.insert(feat_at + 1, feature_decl)
246
+ if constraints_idx is not None:
247
+ constraints_idx += 2
252
248
 
253
- # Insert constraints after "constraints" line
254
- if detected_deps and constraints_idx is not None:
255
- # constraints_idx shifted by 1 due to insertion above
249
+ # Add constraints, creating the section on demand (never leave it empty).
250
+ if detected_deps:
251
+ if constraints_idx is None:
252
+ lines.append("constraints")
253
+ constraints_idx = len(lines) - 1
254
+ ci = constraints_idx
256
255
  for dep in sorted(detected_deps):
257
- constraints_idx += (
258
- 2 # +1 for feature line (2 lines), +1 for each constraint
259
- )
260
- lines.insert(constraints_idx, f"\t{short_name} => {dep}")
256
+ ci += 1
257
+ lines.insert(ci, f"\t{short_name} => {dep}")
261
258
 
262
259
  # Back up the original UVL, then write atomically so a crash mid-write can
263
260
  # never leave a half-written / truncated model.
@@ -0,0 +1,88 @@
1
+ import click
2
+
3
+ from splent_cli.services import context
4
+ from splent_cli.utils.feature_utils import (
5
+ load_product_features,
6
+ parse_feature_entry,
7
+ )
8
+
9
+
10
+ def _load_feature_map(product_name: str) -> dict[tuple[str, str], str]:
11
+ """Return {(org, package): "org/package"} for a product in the workspace.
12
+
13
+ Features are keyed by (organisation, package) so that two features only
14
+ match when BOTH the org/user and the package coincide. The version is
15
+ ignored on purpose — a product on v1.2.0 and another on v1.3.0 of the same
16
+ feature still share that feature.
17
+ """
18
+ product_dir = context.workspace() / product_name
19
+ if not (product_dir / "pyproject.toml").is_file():
20
+ raise click.ClickException(
21
+ f"Product not found: '{product_name}' (no pyproject.toml in {product_dir})"
22
+ )
23
+
24
+ feature_map: dict[tuple[str, str], str] = {}
25
+ for entry in load_product_features(str(product_dir)):
26
+ org, package, _version = parse_feature_entry(entry)
27
+ feature_map[(org, package)] = f"{org}/{package}"
28
+ return feature_map
29
+
30
+
31
+ def _print_block(title: str, color: str, keys: list[tuple[str, str]]) -> None:
32
+ click.secho(f" {title} ({len(keys)})", fg=color, bold=True)
33
+ if not keys:
34
+ click.secho(" —", fg="bright_black")
35
+ for org, package in keys:
36
+ click.echo(f" {org}/{package}")
37
+ click.echo()
38
+
39
+
40
+ @click.command(
41
+ "spl:compare",
42
+ short_help="Compare the features of two products (shared vs product-specific)",
43
+ )
44
+ @click.argument("product_a")
45
+ @click.argument("product_b")
46
+ @context.requires_detached
47
+ def spl_compare(product_a, product_b):
48
+ """Compare two products in the workspace by their feature sets.
49
+
50
+ Reports the features they SHARE and the features SPECIFIC to each one.
51
+ Matching is done by organisation/package — two features are "the same"
52
+ only when both the org (or user) and the package name coincide.
53
+
54
+ \b
55
+ Example:
56
+ splent spl:compare innosoft_app diversolab_app
57
+ """
58
+ a = _load_feature_map(product_a)
59
+ b = _load_feature_map(product_b)
60
+
61
+ a_keys, b_keys = set(a), set(b)
62
+ shared = sorted(a_keys & b_keys)
63
+ only_a = sorted(a_keys - b_keys)
64
+ only_b = sorted(b_keys - a_keys)
65
+
66
+ union = a_keys | b_keys
67
+ reuse_pct = (len(shared) / len(union) * 100) if union else 0.0
68
+
69
+ click.echo()
70
+ click.secho(f" {product_a} ↔ {product_b}", fg="cyan", bold=True)
71
+ click.echo(
72
+ f" {len(a_keys)} feature(s) in {product_a}"
73
+ f" · {len(b_keys)} feature(s) in {product_b}"
74
+ )
75
+ reuse_color = "green" if reuse_pct >= 50 else "yellow"
76
+ click.secho(
77
+ f" Reuse: {len(shared)}/{len(union)} shared ({reuse_pct:.0f}%)",
78
+ fg=reuse_color,
79
+ bold=True,
80
+ )
81
+ click.echo()
82
+
83
+ _print_block("Shared", "green", shared)
84
+ _print_block(f"Only in {product_a}", "yellow", only_a)
85
+ _print_block(f"Only in {product_b}", "magenta", only_b)
86
+
87
+
88
+ cli_command = spl_compare
@@ -35,7 +35,10 @@ def spl_create(name):
35
35
 
36
36
  # {name}.uvl
37
37
  uvl_path = os.path.join(spl_dir, f"{name}.uvl")
38
- uvl_content = f"features\n {name}\n mandatory\n\nconstraints\n"
38
+ # A fresh SPL has no features yet — emit a minimal VALID model (just the
39
+ # root, no empty 'mandatory'/'optional' group, which UVL rejects). Tabs are
40
+ # used for indentation, consistent with spl:add-feature.
41
+ uvl_content = f"features\n\t{name}\n"
39
42
  with open(uvl_path, "w") as f:
40
43
  f.write(uvl_content)
41
44
 
@@ -10,6 +10,10 @@ from splent_framework.utils.path_utils import PathUtils
10
10
  from splent_cli.utils.io_utils import load_toml
11
11
  from splent_cli.utils.proc import run
12
12
 
13
+ # What the operator actually exported, captured before any .env is applied.
14
+ # Precedence from here on: shell environment > product .env > workspace .env.
15
+ _shell_env = dict(os.environ)
16
+
13
17
  load_dotenv()
14
18
 
15
19
  _app_instance = None
@@ -21,7 +25,13 @@ dotenv_path = None
21
25
  if module_name:
22
26
  dotenv_path = PathUtils.get_app_env_file()
23
27
  if os.path.exists(dotenv_path):
28
+ # The product's .env beats the workspace .env, hence override=True.
29
+ # But it must never beat the process environment: a product .env
30
+ # carries container paths (WORKING_DIR=/workspace/), and blindly
31
+ # overriding made the CLI unusable from the host, because whatever
32
+ # WORKING_DIR the operator exported was clobbered on import.
24
33
  load_dotenv(dotenv_path, override=True)
34
+ os.environ.update(_shell_env)
25
35
 
26
36
 
27
37
  def install_features_if_needed():
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: splent_cli
3
- Version: 1.11.0
3
+ Version: 1.13.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
@@ -134,6 +134,7 @@ src/splent_cli/commands/release/release_core.py
134
134
  src/splent_cli/commands/spl/__init__.py
135
135
  src/splent_cli/commands/spl/spl_add_constraints.py
136
136
  src/splent_cli/commands/spl/spl_add_feature.py
137
+ src/splent_cli/commands/spl/spl_compare.py
137
138
  src/splent_cli/commands/spl/spl_configurations.py
138
139
  src/splent_cli/commands/spl/spl_create.py
139
140
  src/splent_cli/commands/spl/spl_deps.py