splent-cli 1.34.0__tar.gz → 1.35.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 (283) hide show
  1. {splent_cli-1.34.0/src/splent_cli.egg-info → splent_cli-1.35.0}/PKG-INFO +1 -1
  2. {splent_cli-1.34.0 → splent_cli-1.35.0}/pyproject.toml +1 -1
  3. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/cli.py +48 -1
  4. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/commands/feature/feature_compat.py +135 -0
  5. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/commands/feature/feature_create.py +9 -5
  6. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/commands/feature/feature_detach.py +4 -3
  7. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/commands/feature/feature_env.py +4 -1
  8. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/commands/feature/feature_refine.py +7 -0
  9. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/commands/feature/feature_remove.py +5 -4
  10. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/commands/feature/feature_test.py +5 -20
  11. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/commands/product/product_build.py +1 -20
  12. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/commands/product/product_create.py +2 -5
  13. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/commands/product/product_resolve.py +8 -1
  14. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/commands/product/product_sync_template.py +23 -1
  15. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/commands/product/product_validate.py +9 -1
  16. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/commands/release/release_resume.py +19 -15
  17. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/services/marketplace.py +2 -6
  18. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/services/release.py +61 -16
  19. splent_cli-1.35.0/src/splent_cli/services/web_container.py +66 -0
  20. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/templates/product/product_ci_upgrade.yml.j2 +7 -1
  21. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/templates/product/product_ci_validate.yml.j2 +15 -0
  22. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/utils/cache_utils.py +34 -0
  23. splent_cli-1.35.0/src/splent_cli/utils/feature_naming.py +58 -0
  24. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/utils/integrity.py +2 -2
  25. splent_cli-1.35.0/src/splent_cli/utils/running_version.py +28 -0
  26. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/utils/template_drift.py +2 -5
  27. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/utils/upgrade_plan.py +3 -0
  28. {splent_cli-1.34.0 → splent_cli-1.35.0/src/splent_cli.egg-info}/PKG-INFO +1 -1
  29. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli.egg-info/SOURCES.txt +3 -0
  30. {splent_cli-1.34.0 → splent_cli-1.35.0}/COPYING +0 -0
  31. {splent_cli-1.34.0 → splent_cli-1.35.0}/COPYING.LESSER +0 -0
  32. {splent_cli-1.34.0 → splent_cli-1.35.0}/README.md +0 -0
  33. {splent_cli-1.34.0 → splent_cli-1.35.0}/setup.cfg +0 -0
  34. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/__init__.py +0 -0
  35. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/__main__.py +0 -0
  36. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/commands/__init__.py +0 -0
  37. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/commands/cache/__init__.py +0 -0
  38. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/commands/cache/cache_clear.py +0 -0
  39. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/commands/cache/cache_orphans.py +0 -0
  40. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/commands/cache/cache_outdated.py +0 -0
  41. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/commands/cache/cache_prune.py +0 -0
  42. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/commands/cache/cache_size.py +0 -0
  43. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/commands/cache/cache_status.py +0 -0
  44. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/commands/cache/cache_usage.py +0 -0
  45. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/commands/cache/cache_versions.py +0 -0
  46. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/commands/check/__init__.py +0 -0
  47. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/commands/check/check_contracts.py +0 -0
  48. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/commands/check/check_deps.py +0 -0
  49. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/commands/check/check_docker.py +0 -0
  50. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/commands/check/check_env.py +0 -0
  51. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/commands/check/check_features.py +0 -0
  52. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/commands/check/check_github.py +0 -0
  53. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/commands/check/check_infra.py +0 -0
  54. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/commands/check/check_product.py +0 -0
  55. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/commands/check/check_pypi.py +0 -0
  56. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/commands/check/check_pyproject.py +0 -0
  57. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/commands/check/check_releases.py +0 -0
  58. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/commands/clear/__init__.py +0 -0
  59. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/commands/clear/clear_build.py +0 -0
  60. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/commands/clear/clear_log.py +0 -0
  61. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/commands/clear/clear_uploads.py +0 -0
  62. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/commands/command/__init__.py +0 -0
  63. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/commands/command/command_create.py +0 -0
  64. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/commands/coverage.py +0 -0
  65. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/commands/database/db_console.py +0 -0
  66. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/commands/database/db_dump.py +0 -0
  67. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/commands/database/db_migrate.py +0 -0
  68. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/commands/database/db_reset.py +0 -0
  69. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/commands/database/db_restore.py +0 -0
  70. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/commands/database/db_rollback.py +0 -0
  71. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/commands/database/db_seed.py +0 -0
  72. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/commands/database/db_status.py +0 -0
  73. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/commands/database/db_upgrade.py +0 -0
  74. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/commands/doctor.py +0 -0
  75. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/commands/env/env_list.py +0 -0
  76. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/commands/env/env_set.py +0 -0
  77. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/commands/env/env_show.py +0 -0
  78. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/commands/export/__init__.py +0 -0
  79. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/commands/export/export_puml.py +0 -0
  80. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/commands/feature/feature_add.py +0 -0
  81. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/commands/feature/feature_attach.py +0 -0
  82. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/commands/feature/feature_clean.py +0 -0
  83. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/commands/feature/feature_clone.py +0 -0
  84. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/commands/feature/feature_compile.py +0 -0
  85. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/commands/feature/feature_contract.py +0 -0
  86. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/commands/feature/feature_delete.py +0 -0
  87. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/commands/feature/feature_discard.py +0 -0
  88. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/commands/feature/feature_drift.py +0 -0
  89. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/commands/feature/feature_fork.py +0 -0
  90. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/commands/feature/feature_git.py +0 -0
  91. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/commands/feature/feature_hook_add.py +0 -0
  92. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/commands/feature/feature_hook_remove.py +0 -0
  93. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/commands/feature/feature_hooks.py +0 -0
  94. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/commands/feature/feature_impact.py +0 -0
  95. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/commands/feature/feature_info.py +0 -0
  96. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/commands/feature/feature_inject_config.py +0 -0
  97. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/commands/feature/feature_install.py +0 -0
  98. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/commands/feature/feature_list.py +0 -0
  99. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/commands/feature/feature_order.py +0 -0
  100. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/commands/feature/feature_outdated.py +0 -0
  101. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/commands/feature/feature_pin.py +0 -0
  102. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/commands/feature/feature_pip_install.py +0 -0
  103. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/commands/feature/feature_pull.py +0 -0
  104. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/commands/feature/feature_release.py +0 -0
  105. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/commands/feature/feature_rename.py +0 -0
  106. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/commands/feature/feature_search.py +0 -0
  107. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/commands/feature/feature_status.py +0 -0
  108. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/commands/feature/feature_sync_template.py +0 -0
  109. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/commands/feature/feature_translate.py +0 -0
  110. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/commands/feature/feature_unlock.py +0 -0
  111. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/commands/feature/feature_upgrade.py +0 -0
  112. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/commands/feature/feature_versions.py +0 -0
  113. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/commands/feature/feature_xray.py +0 -0
  114. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/commands/lint.py +0 -0
  115. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/commands/locust.py +0 -0
  116. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/commands/marketplace/login.py +0 -0
  117. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/commands/marketplace/logout.py +0 -0
  118. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/commands/marketplace/marketplace_auth_utils.py +0 -0
  119. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/commands/marketplace/marketplace_index.py +0 -0
  120. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/commands/marketplace/whoami.py +0 -0
  121. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/commands/product/__init__.py +0 -0
  122. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/commands/product/product_auto_require.py +0 -0
  123. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/commands/product/product_clean.py +0 -0
  124. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/commands/product/product_commands.py +0 -0
  125. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/commands/product/product_config.py +0 -0
  126. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/commands/product/product_configure.py +0 -0
  127. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/commands/product/product_console.py +0 -0
  128. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/commands/product/product_containers.py +0 -0
  129. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/commands/product/product_deploy.py +0 -0
  130. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/commands/product/product_derive.py +0 -0
  131. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/commands/product/product_deselect.py +0 -0
  132. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/commands/product/product_down.py +0 -0
  133. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/commands/product/product_drift.py +0 -0
  134. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/commands/product/product_env.py +0 -0
  135. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/commands/product/product_list.py +0 -0
  136. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/commands/product/product_logs.py +0 -0
  137. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/commands/product/product_missing.py +0 -0
  138. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/commands/product/product_port.py +0 -0
  139. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/commands/product/product_release.py +0 -0
  140. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/commands/product/product_restart.py +0 -0
  141. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/commands/product/product_routes.py +0 -0
  142. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/commands/product/product_run.py +0 -0
  143. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/commands/product/product_select.py +0 -0
  144. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/commands/product/product_shell.py +0 -0
  145. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/commands/product/product_signals.py +0 -0
  146. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/commands/product/product_test.py +0 -0
  147. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/commands/product/product_up.py +0 -0
  148. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/commands/release/__init__.py +0 -0
  149. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/commands/release/release_core.py +0 -0
  150. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/commands/selenium.py +0 -0
  151. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/commands/spl/__init__.py +0 -0
  152. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/commands/spl/spl_add_constraints.py +0 -0
  153. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/commands/spl/spl_add_feature.py +0 -0
  154. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/commands/spl/spl_compare.py +0 -0
  155. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/commands/spl/spl_configurations.py +0 -0
  156. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/commands/spl/spl_create.py +0 -0
  157. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/commands/spl/spl_deps.py +0 -0
  158. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/commands/spl/spl_features.py +0 -0
  159. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/commands/spl/spl_fetch.py +0 -0
  160. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/commands/spl/spl_info.py +0 -0
  161. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/commands/spl/spl_list.py +0 -0
  162. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/commands/spl/spl_outdated.py +0 -0
  163. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/commands/spl/spl_pin.py +0 -0
  164. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/commands/spl/spl_publish.py +0 -0
  165. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/commands/spl/spl_remove_feature.py +0 -0
  166. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/commands/spl/spl_utils.py +0 -0
  167. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/commands/tokens_setup.py +0 -0
  168. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/commands/uvl/uvl_utils.py +0 -0
  169. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/commands/version.py +0 -0
  170. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/services/__init__.py +0 -0
  171. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/services/compose.py +0 -0
  172. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/services/context.py +0 -0
  173. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/services/credentials.py +0 -0
  174. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/services/deploy_bundle.py +0 -0
  175. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/services/image_registry.py +0 -0
  176. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/services/index_refresh.py +0 -0
  177. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/services/marketplace_api.py +0 -0
  178. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/services/marketplace_url.py +0 -0
  179. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/services/preflight.py +0 -0
  180. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/services/pypi_quota.py +0 -0
  181. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/services/registry.py +0 -0
  182. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/services/release_dry_run.py +0 -0
  183. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/services/release_gate.py +0 -0
  184. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/services/spl_store.py +0 -0
  185. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/templates/feature/feature_.gitignore.j2 +0 -0
  186. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/templates/feature/feature_MANIFEST.in.j2 +0 -0
  187. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/templates/feature/feature_README.md.j2 +0 -0
  188. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/templates/feature/feature_commands.py.j2 +0 -0
  189. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/templates/feature/feature_config.py.j2 +0 -0
  190. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/templates/feature/feature_docker-compose.dev.yml.j2 +0 -0
  191. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/templates/feature/feature_docker-compose.prod.yml.j2 +0 -0
  192. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/templates/feature/feature_docker.env.example.j2 +0 -0
  193. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/templates/feature/feature_forms.py.j2 +0 -0
  194. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/templates/feature/feature_hooks.py.j2 +0 -0
  195. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/templates/feature/feature_init.py.j2 +0 -0
  196. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/templates/feature/feature_init_config.py.j2 +0 -0
  197. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/templates/feature/feature_init_light.py.j2 +0 -0
  198. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/templates/feature/feature_init_service.py.j2 +0 -0
  199. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/templates/feature/feature_migrations_alembic.ini.j2 +0 -0
  200. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/templates/feature/feature_migrations_env.py.j2 +0 -0
  201. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/templates/feature/feature_migrations_script.py.mako.j2 +0 -0
  202. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/templates/feature/feature_models.py.j2 +0 -0
  203. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/templates/feature/feature_pyproject.toml.j2 +0 -0
  204. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/templates/feature/feature_pyproject_minimal.toml.j2 +0 -0
  205. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/templates/feature/feature_repositories.py.j2 +0 -0
  206. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/templates/feature/feature_routes.py.j2 +0 -0
  207. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/templates/feature/feature_scripts.js.j2 +0 -0
  208. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/templates/feature/feature_seeders.py.j2 +0 -0
  209. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/templates/feature/feature_services.py.j2 +0 -0
  210. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/templates/feature/feature_signals.py.j2 +0 -0
  211. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/templates/feature/feature_templates_index.html.j2 +0 -0
  212. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/templates/feature/feature_tests_conftest.py.j2 +0 -0
  213. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/templates/feature/feature_tests_e2e.py.j2 +0 -0
  214. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/templates/feature/feature_tests_functional.py.j2 +0 -0
  215. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/templates/feature/feature_tests_integration.py.j2 +0 -0
  216. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/templates/feature/feature_tests_locustfile.py.j2 +0 -0
  217. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/templates/feature/feature_tests_unit.py.j2 +0 -0
  218. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/templates/feature/feature_webpack.config.js.j2 +0 -0
  219. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/templates/licenses/COPYING +0 -0
  220. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/templates/licenses/COPYING.LESSER +0 -0
  221. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/templates/product/product_.env.dev.example.j2 +0 -0
  222. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/templates/product/product_.env.prod.example.j2 +0 -0
  223. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/templates/product/product_.gitignore.j2 +0 -0
  224. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/templates/product/product_00_core_requirements_dev.sh.j2 +0 -0
  225. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/templates/product/product_00_install_features.sh.j2 +0 -0
  226. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/templates/product/product_01_compile_assets.sh.j2 +0 -0
  227. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/templates/product/product_02_0_db_wait_connection.sh.j2 +0 -0
  228. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/templates/product/product_02_1_db_create_db_test.sh.j2 +0 -0
  229. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/templates/product/product_02_2_db_create_splent_migrations.sh.j2 +0 -0
  230. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/templates/product/product_03_initialize_migrations.sh.j2 +0 -0
  231. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/templates/product/product_04_handle_migrations.sh.j2 +0 -0
  232. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/templates/product/product_05_0_start_app_dev.sh.j2 +0 -0
  233. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/templates/product/product_05_1_start_app_prod.sh.j2 +0 -0
  234. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/templates/product/product_400.html +0 -0
  235. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/templates/product/product_401.html +0 -0
  236. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/templates/product/product_404.html +0 -0
  237. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/templates/product/product_500.html +0 -0
  238. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/templates/product/product_Dockerfile.dev.dockerignore.j2 +0 -0
  239. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/templates/product/product_Dockerfile.dev.j2 +0 -0
  240. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/templates/product/product_Dockerfile.prod.dockerignore.j2 +0 -0
  241. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/templates/product/product_Dockerfile.prod.j2 +0 -0
  242. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/templates/product/product_README.md.j2 +0 -0
  243. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/templates/product/product_app.css +0 -0
  244. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/templates/product/product_app.js +0 -0
  245. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/templates/product/product_base_template.html +0 -0
  246. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/templates/product/product_config.py.j2 +0 -0
  247. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/templates/product/product_docker-compose.dev.yml.j2 +0 -0
  248. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/templates/product/product_docker-compose.prod.yml.j2 +0 -0
  249. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/templates/product/product_dropzone.css +0 -0
  250. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/templates/product/product_entrypoint.dev.sh.j2 +0 -0
  251. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/templates/product/product_entrypoint.prod.sh.j2 +0 -0
  252. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/templates/product/product_errors.py.j2 +0 -0
  253. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/templates/product/product_favicon.svg +0 -0
  254. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/templates/product/product_init.py.j2 +0 -0
  255. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/templates/product/product_logging.py.j2 +0 -0
  256. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/templates/product/product_own.css +0 -0
  257. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/templates/product/product_package.json.j2 +0 -0
  258. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/templates/product/product_pyproject.toml.j2 +0 -0
  259. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/templates/product/product_uvl.j2 +0 -0
  260. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/utils/__init__.py +0 -0
  261. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/utils/archetype.py +0 -0
  262. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/utils/command_loader.py +0 -0
  263. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/utils/contract_freshness.py +0 -0
  264. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/utils/db_client.py +0 -0
  265. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/utils/db_utils.py +0 -0
  266. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/utils/decorators.py +0 -0
  267. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/utils/dynamic_imports.py +0 -0
  268. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/utils/feature_installer.py +0 -0
  269. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/utils/feature_utils.py +0 -0
  270. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/utils/forge.py +0 -0
  271. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/utils/git_url.py +0 -0
  272. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/utils/io_utils.py +0 -0
  273. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/utils/lifecycle.py +0 -0
  274. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/utils/manifest.py +0 -0
  275. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/utils/path_utils.py +0 -0
  276. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/utils/proc.py +0 -0
  277. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/utils/redact.py +0 -0
  278. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/utils/refinement_facts.py +0 -0
  279. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli/utils/toml_edit.py +0 -0
  280. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli.egg-info/dependency_links.txt +0 -0
  281. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli.egg-info/entry_points.txt +0 -0
  282. {splent_cli-1.34.0 → splent_cli-1.35.0}/src/splent_cli.egg-info/requires.txt +0 -0
  283. {splent_cli-1.34.0 → splent_cli-1.35.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.34.0
3
+ Version: 1.35.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
  License-Expression: LGPL-3.0-or-later
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "splent_cli"
7
- version = "1.34.0"
7
+ version = "1.35.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"
@@ -11,6 +11,15 @@ from splent_cli.utils.command_loader import load_commands
11
11
  load_dotenv()
12
12
 
13
13
 
14
+ def _missing_module(exc: BaseException) -> str | None:
15
+ """The module whose absence made building the app fail, if that is why."""
16
+ while exc is not None:
17
+ if isinstance(exc, ModuleNotFoundError) and exc.name:
18
+ return exc.name
19
+ exc = exc.__cause__ or exc.__context__
20
+ return None
21
+
22
+
14
23
  class SPLENTCLI(click.Group):
15
24
  """
16
25
  Main SPLENT CLI class.
@@ -101,15 +110,53 @@ class SPLENTCLI(click.Group):
101
110
 
102
111
  # ── App context injection ─────────────────────────────────────
103
112
 
113
+ def _delegate_to_the_product(self, ctx) -> None:
114
+ """Run this command in the product's web container, and exit with its
115
+ code; return when that container is not running.
116
+
117
+ Called only when the app cannot be built here: building it imports
118
+ what the features declare, which the product's image installs and the
119
+ CLI's may not (see services/web_container.py).
120
+ """
121
+ from splent_cli.services import web_container
122
+
123
+ product = os.getenv("SPLENT_APP")
124
+ if not product or web_container.is_product_container():
125
+ return
126
+ if not web_container.running(web_container.name(product)):
127
+ return
128
+ argv = [*ctx.protected_args, *ctx.args]
129
+ raise SystemExit(web_container.delegate(product, argv))
130
+
104
131
  def invoke(self, ctx):
105
132
  cmd_name = ctx.protected_args[0] if ctx.protected_args else None
106
133
  command = self.get_command(ctx, cmd_name)
107
134
 
135
+ # A feature's own commands are only found by building the app, so
136
+ # here they may be missing although the product has them.
137
+ if command is None and cmd_name and cmd_name.startswith("feature:"):
138
+ self._delegate_to_the_product(ctx)
139
+
108
140
  if command and getattr(command, "requires_app", False):
109
141
  from splent_cli.utils.dynamic_imports import get_app
110
142
  from splent_cli.utils.db_utils import check_db_connection
111
143
 
112
- app = get_app()
144
+ try:
145
+ app = get_app()
146
+ except Exception as exc:
147
+ missing = _missing_module(exc)
148
+ if missing is None:
149
+ raise
150
+ # Only then: where the app builds here, the command runs here
151
+ # with the tools the CLI image has (ruff formats migrations).
152
+ self._delegate_to_the_product(ctx)
153
+ product = os.getenv("SPLENT_APP", "The product")
154
+ raise click.ClickException(
155
+ f"{product}'s features need {missing}, which the product's "
156
+ "image installs and the CLI's does not.\n"
157
+ " Start the product (splent product:up --dev) and run the "
158
+ "command again: it then runs in the product's web container."
159
+ ) from exc
113
160
  if getattr(command, "requires_db", False):
114
161
  if not check_db_connection(app):
115
162
  raise SystemExit(1)
@@ -411,6 +411,141 @@ def _analyse_all(labeled_contracts: list[tuple[str, dict]]) -> list[dict]:
411
411
  return sorted(findings, key=lambda f: SEVERITY_ORDER[f["severity"]])
412
412
 
413
413
 
414
+ def _contract_env(path: Path) -> str | None:
415
+ """The environment a feature's contract confines it to, if any."""
416
+ pyproject = path / "pyproject.toml"
417
+ if not pyproject.exists():
418
+ return None
419
+ data = load_toml(pyproject, what=f"pyproject.toml for {path.name}")
420
+ return data.get("tool", {}).get("splent", {}).get("contract", {}).get("env")
421
+
422
+
423
+ def env_scope_findings(workspace: str, product_dir: str) -> list[dict]:
424
+ """Features confined to one environment, in a list that installs them in
425
+ the other.
426
+
427
+ mailhog and phpmyadmin declare env = "dev" in their contract. In the base
428
+ list the production image installed them too, phpMyAdmin with autologin
429
+ to the database, and nothing said so.
430
+ """
431
+ from splent_framework.utils.pyproject_reader import PyprojectReader
432
+
433
+ reader = PyprojectReader.for_product(product_dir)
434
+ lists = {
435
+ "features": reader.features,
436
+ "features_dev": reader.features_dev,
437
+ "features_prod": reader.features_prod,
438
+ }
439
+ findings = []
440
+ for key, entries in lists.items():
441
+ for entry in entries:
442
+ ns, name, version = _parse_pyproject_entry(entry)
443
+ path = product_feature_dir(workspace, product_dir, ns, name, version)
444
+ if path is None:
445
+ continue
446
+ scope = _contract_env(path)
447
+ short = name.removeprefix("splent_feature_")
448
+ if scope == "dev" and key != "features_dev":
449
+ message = (
450
+ f"{short} is a development tool (its contract says env = "
451
+ f'"dev"), but [tool.splent] {key} puts it in the production '
452
+ "image too. Move it to features_dev."
453
+ )
454
+ elif scope == "prod" and key == "features_dev":
455
+ message = (
456
+ f"{short} only runs in production (its contract says env = "
457
+ '"prod"), but it is in [tool.splent] features_dev. Move it '
458
+ "to features_prod."
459
+ )
460
+ else:
461
+ continue
462
+ findings.append(
463
+ {
464
+ "severity": "error",
465
+ "field": "env",
466
+ "message": message,
467
+ "features": [short],
468
+ }
469
+ )
470
+ return findings
471
+
472
+
473
+ def _joins_the_shared_network(compose_file: Path) -> bool:
474
+ """Whether a compose file puts its services on the shared splent_network
475
+ instead of the product's own (``name: ${SPLENT_NETWORK:-splent_network}``)."""
476
+ import yaml
477
+
478
+ try:
479
+ data = yaml.safe_load(compose_file.read_text()) or {}
480
+ except (OSError, yaml.YAMLError):
481
+ return False
482
+ networks = data.get("networks") or {}
483
+ spec = networks.get("splent_network")
484
+ if not isinstance(spec, dict) or not spec.get("external"):
485
+ return False
486
+ return "SPLENT_NETWORK" not in str(spec.get("name", ""))
487
+
488
+
489
+ def network_findings(workspace: str, product_dir: str) -> list[dict]:
490
+ """Feature services the product cannot reach.
491
+
492
+ Each product has a network of its own, and a feature's services join it
493
+ through SPLENT_NETWORK. mailhog and phpmyadmin named the shared network
494
+ instead, so the app could not send mail to Mailhog and phpMyAdmin could
495
+ not reach the database, and nothing said why.
496
+ """
497
+ from splent_framework.utils.pyproject_reader import PyprojectReader
498
+
499
+ reader = PyprojectReader.for_product(product_dir)
500
+ findings = []
501
+ for entry in reader.features + reader.features_dev + reader.features_prod:
502
+ ns, name, version = _parse_pyproject_entry(entry)
503
+ path = product_feature_dir(workspace, product_dir, ns, name, version)
504
+ if path is None:
505
+ continue
506
+ files = sorted((path / "docker").glob("docker-compose*.yml"))
507
+ if any(_joins_the_shared_network(f) for f in files):
508
+ short = name.removeprefix("splent_feature_")
509
+ findings.append(
510
+ {
511
+ "severity": "error",
512
+ "field": "docker",
513
+ "message": (
514
+ f"{short}'s services join the shared splent_network, "
515
+ "where the product's app cannot reach them. Its compose "
516
+ "file needs name: ${SPLENT_NETWORK:-splent_network} "
517
+ "under networks.splent_network."
518
+ ),
519
+ "features": [short],
520
+ }
521
+ )
522
+ return findings
523
+
524
+
525
+ def naming_findings(workspace: str, product_dir: str) -> list[dict]:
526
+ """Features named after a product (auth_uvlhub). A warning, so the debt
527
+ shows in every validation until the product drops them."""
528
+ from splent_framework.utils.pyproject_reader import PyprojectReader
529
+
530
+ from splent_cli.utils.feature_naming import product_suffix, product_suffix_message
531
+
532
+ reader = PyprojectReader.for_product(product_dir)
533
+ findings = []
534
+ for entry in reader.features + reader.features_dev + reader.features_prod:
535
+ _, name, _ = _parse_pyproject_entry(entry)
536
+ product = product_suffix(name, workspace)
537
+ if product:
538
+ findings.append(
539
+ {
540
+ "severity": "warning",
541
+ "field": "name",
542
+ "message": product_suffix_message(name, product),
543
+ "features": [name.removeprefix("splent_feature_")],
544
+ }
545
+ )
546
+ return findings
547
+
548
+
414
549
  def run_all_product_check(workspace: str, product_dir: str) -> list[dict]:
415
550
  """Run the full product compatibility check. Returns findings list."""
416
551
  try:
@@ -9,13 +9,11 @@ from splent_cli.utils.feature_utils import (
9
9
  require_valid_feature_ref,
10
10
  )
11
11
  from splent_cli.services import context
12
+ from splent_cli.utils.feature_naming import product_suffix, product_suffix_message
12
13
 
13
- try:
14
- from importlib.metadata import version as _pkg_version
14
+ from splent_cli.utils.running_version import cli_version as _running_cli_version
15
15
 
16
- _CLI_VERSION = _pkg_version("splent_cli")
17
- except Exception:
18
- _CLI_VERSION = "dev"
16
+ _CLI_VERSION = _running_cli_version()
19
17
 
20
18
 
21
19
  FEATURE_TYPES = ("full", "light", "config", "service")
@@ -226,6 +224,12 @@ def make_feature(full_name, feature_type, docker):
226
224
 
227
225
  # --- Target directory (workspace root for editable features) ---
228
226
  workspace = str(context.workspace())
227
+
228
+ product = product_suffix(feature_name, workspace)
229
+ if product:
230
+ click.secho(f"❌ {product_suffix_message(feature_name, product)}", fg="red")
231
+ raise SystemExit(1)
232
+
229
233
  feature_dir = os.path.join(workspace, feature_name)
230
234
  src_path = os.path.join(feature_dir, "src", org_safe, feature_name)
231
235
 
@@ -25,7 +25,7 @@ from splent_cli.utils.manifest import (
25
25
 
26
26
  @click.command(
27
27
  "feature:detach",
28
- short_help="Unregister a versioned feature from the active product (cache is kept).",
28
+ short_help="Unpin a released version: the entry goes back to editable (cache is kept).",
29
29
  )
30
30
  @click.argument("feature_identifier", required=True)
31
31
  @click.argument("version", required=True)
@@ -36,9 +36,10 @@ from splent_cli.utils.manifest import (
36
36
  )
37
37
  def feature_detach(feature_identifier, version, force):
38
38
  """
39
- Detach a versioned feature from the active product.
39
+ Unpin a released version, the reverse of feature:attach.
40
40
 
41
- - Removes namespace/feature@version from pyproject.toml
41
+ - Drops @version from the entry in pyproject.toml, which stays as the
42
+ bare, editable namespace/feature (feature:remove takes it out)
42
43
  - Removes symlink in features/<namespace>/
43
44
  - Leaves .splent_cache intact
44
45
  """
@@ -119,7 +119,10 @@ def feature_env(feature_name, generate, env_name):
119
119
 
120
120
  if selected:
121
121
  try:
122
- shutil.copy(selected, env_file)
122
+ # copyfile, not copy: a pinned feature's cache is read-only, and
123
+ # copying the mode made its .env read-only too, so product:env
124
+ # --merge failed writing __FEATURE_HOST_DIR__ into it.
125
+ shutil.copyfile(selected, env_file)
123
126
  except OSError as exc:
124
127
  click.echo(f"❌ Failed to create {env_file} from {selected}: {exc}")
125
128
  raise SystemExit(1)
@@ -14,6 +14,7 @@ from pathlib import Path
14
14
  import click
15
15
 
16
16
  from splent_cli.services import context
17
+ from splent_cli.utils.feature_naming import product_suffix, product_suffix_message
17
18
  from splent_cli.utils.feature_utils import normalize_namespace, read_features_from_data
18
19
  from splent_cli.utils.refinement_facts import (
19
20
  FeatureSource,
@@ -1009,6 +1010,12 @@ def feature_refinement(refiner_name):
1009
1010
  # A feature other features refine cannot refine anything (a chain)
1010
1011
  _refuse_if_refined(_get_product_features(workspace, product), refiner_name)
1011
1012
 
1013
+ # A refiner named after a product is a product patch, not variability.
1014
+ named_after = product_suffix(refiner_name, workspace)
1015
+ if named_after:
1016
+ click.secho(f" {product_suffix_message(refiner_name, named_after)}", fg="red")
1017
+ raise SystemExit(1)
1018
+
1012
1019
  # Check if refinement already configured
1013
1020
  refiner_data = load_toml(refiner_pyproject, what=f"{refiner_name} pyproject.toml")
1014
1021
  existing_refinement = (
@@ -24,7 +24,7 @@ from splent_cli.utils.manifest import (
24
24
 
25
25
  @click.command(
26
26
  "feature:remove",
27
- short_help="Unregister an editable feature from the active product (keeps files).",
27
+ short_help="Remove a feature from the active product, pinned or editable (keeps files).",
28
28
  )
29
29
  @click.argument("feature_name", required=True)
30
30
  @click.option("--namespace", "-n", help="Namespace (defaults to 'splent-io').")
@@ -47,9 +47,10 @@ from splent_cli.utils.manifest import (
47
47
  )
48
48
  def feature_remove(feature_name, namespace, env_scope, force):
49
49
  """
50
- Removes a local feature (no version, no repo) from the current SPLENT product:
51
- - Removes entry from [features] in pyproject.toml
52
- - Removes symlink under /workspace/<product>/features/<namespace>/<feature_name>
50
+ Removes a feature from the current SPLENT product, pinned or editable:
51
+ - Removes its entry from the feature lists in pyproject.toml
52
+ - Removes its symlink under /workspace/<product>/features/<namespace>/
53
+ The checkout and the cached release stay on disk.
53
54
 
54
55
  \b
55
56
  Without flags every list is searched (features, features_dev, features_prod).
@@ -13,7 +13,7 @@ from splent_cli.utils.feature_utils import (
13
13
  get_normalize_feature_name_in_splent_format,
14
14
  parse_feature_entry,
15
15
  )
16
- from splent_cli.services import context
16
+ from splent_cli.services import context, web_container
17
17
 
18
18
 
19
19
  # Test levels in execution order (default = first three)
@@ -21,28 +21,13 @@ TEST_LEVELS = ("unit", "integration", "functional", "e2e", "load")
21
21
  DEFAULT_LEVELS = ("unit", "integration", "functional")
22
22
 
23
23
 
24
- def _is_product_container() -> bool:
25
- """Whether this process already runs inside the product's web container.
26
-
27
- SPLENT_CONTAINER is the explicit answer when set (the CLI container says
28
- "cli", product images say "product"); /.dockerenv covers containers older
29
- than the variable.
30
- """
31
- container = os.getenv("SPLENT_CONTAINER")
32
- if container:
33
- return container != "cli"
34
- return os.path.exists("/.dockerenv")
24
+ # Shared with every command that needs the product's app (see cli.py).
25
+ _is_product_container = web_container.is_product_container
26
+ _container_running = web_container.running
35
27
 
36
28
 
37
29
  def _web_container() -> str:
38
- return f"{context.require_app()}_web"
39
-
40
-
41
- def _container_running(name: str) -> bool:
42
- result = run(
43
- ["docker", "ps", "-q", "-f", f"name=^{name}$"], check=False, capture=True
44
- )
45
- return result.returncode == 0 and bool(result.stdout.strip())
30
+ return web_container.name(context.require_app())
46
31
 
47
32
 
48
33
  _PASSTHROUGH_ENV = ("PYTEST_ADDOPTS", "PYTHONDONTWRITEBYTECODE")
@@ -9,6 +9,7 @@ import click
9
9
  from splent_cli.services import context, compose, deploy_bundle
10
10
  from splent_cli.services.preflight import run_preflight
11
11
  from splent_cli.utils.feature_utils import read_features_from_data
12
+ from splent_cli.utils.running_version import cli_version
12
13
  from splent_cli.utils.io_utils import (
13
14
  atomic_write,
14
15
  env_line,
@@ -406,26 +407,6 @@ def framework_version(workspace: str) -> str:
406
407
  )
407
408
 
408
409
 
409
- def cli_version() -> str:
410
- """The version of the CLI that is running, the one the image installs too.
411
-
412
- Read from the source tree it runs from when that is a checkout: an
413
- editable install records its version once, when it is installed, and the
414
- checkout moves on with every release.
415
- """
416
- from importlib.metadata import version
417
- from pathlib import Path
418
-
419
- import splent_cli
420
-
421
- pyproject = Path(splent_cli.__file__).resolve().parents[2] / "pyproject.toml"
422
- if pyproject.is_file():
423
- project = load_toml(str(pyproject)).get("project", {})
424
- if project.get("name") == "splent_cli" and project.get("version"):
425
- return str(project["version"])
426
- return version("splent_cli")
427
-
428
-
429
410
  def runtime_pins(workspace: str) -> dict:
430
411
  """{package: version} the production image installs from PyPI."""
431
412
  return {
@@ -20,12 +20,9 @@ from splent_cli.utils.template_drift import (
20
20
  )
21
21
  from splent_cli.utils.template_drift import _forge_host as _template_forge_host
22
22
 
23
- try:
24
- from importlib.metadata import version as _pkg_version
23
+ from splent_cli.utils.running_version import cli_version as _running_cli_version
25
24
 
26
- _CLI_VERSION = _pkg_version("splent_cli")
27
- except Exception:
28
- _CLI_VERSION = "dev"
25
+ _CLI_VERSION = _running_cli_version()
29
26
 
30
27
 
31
28
  PRODUCT_NAME_RE = re.compile(r"^[a-z_][a-z0-9_]*$")
@@ -9,7 +9,7 @@ from splent_cli.utils.feature_utils import (
9
9
  parse_feature_entry,
10
10
  read_features_from_data,
11
11
  )
12
- from splent_cli.utils.cache_utils import make_feature_writable
12
+ from splent_cli.utils.cache_utils import make_feature_writable, restore_snapshot
13
13
 
14
14
 
15
15
  def _splent_list(data: dict, key: str) -> list[str]:
@@ -194,11 +194,18 @@ def product_sync(ctx, force, yes, mode=None, strict=False):
194
194
  failed += 1
195
195
  continue
196
196
 
197
+ restored = [] if cloned else restore_snapshot(cache_dir)
197
198
  _create_symlink(cache_dir, product_features_dir, link_path)
198
199
  marker = click.style(" ✓", fg="green")
199
200
  label = f" {short:<{max_w_r}}"
200
201
  ver = click.style(f" {version}", dim=True)
201
202
  src = click.style(" cached" if not cloned else " cloned", dim=True)
203
+ if restored:
204
+ src += click.style(
205
+ f" restored {len(restored)} file(s) to the release: "
206
+ + ", ".join(restored),
207
+ fg="yellow",
208
+ )
202
209
  tag = _feature_env_tag(entry, dev_set, prod_set)
203
210
  click.echo(f" {marker}{label}{ver}{src}{tag}")
204
211
 
@@ -2,8 +2,9 @@ import click
2
2
  from pathlib import Path
3
3
 
4
4
  from splent_cli.services import context
5
- from splent_cli.utils.io_utils import atomic_write
5
+ from splent_cli.utils.io_utils import atomic_write, load_toml
6
6
  from splent_cli.utils.template_drift import (
7
+ CLI_VERSION,
7
8
  PRODUCT_GROUPS,
8
9
  count_changed_lines,
9
10
  file_diff,
@@ -12,6 +13,23 @@ from splent_cli.utils.template_drift import (
12
13
  resolve_product_dir,
13
14
  resolve_product_rel,
14
15
  )
16
+ from splent_cli.utils.toml_edit import write_toml
17
+
18
+
19
+ def _record_cli_version(product_path: Path) -> None:
20
+ """Store the CLI whose templates the product now matches.
21
+
22
+ product:drift reports it as "generated with CLI vX". Only product:create
23
+ wrote it, so a product brought fully up to date kept naming the CLI it was
24
+ created with. A partial sync (--scripts, --docker...) leaves it alone,
25
+ because the other groups may still be older.
26
+ """
27
+ pyproject = product_path / "pyproject.toml"
28
+ data = load_toml(pyproject, what="pyproject.toml")
29
+ splent = data.setdefault("tool", {}).setdefault("splent", {})
30
+ if splent.get("cli_version") != CLI_VERSION:
31
+ splent["cli_version"] = CLI_VERSION
32
+ write_toml(pyproject, data)
15
33
 
16
34
 
17
35
  @click.command(
@@ -112,6 +130,8 @@ def product_sync_template(
112
130
  changes[rel_path] = (abs_path, expected, n)
113
131
 
114
132
  if not changes:
133
+ if not filter_active and not dry_run:
134
+ _record_cli_version(product_path)
115
135
  click.echo()
116
136
  click.secho(" ✅ All SPLENT-owned files are already up to date.", fg="green")
117
137
  click.echo()
@@ -138,6 +158,8 @@ def product_sync_template(
138
158
  # Atomic: an interrupted run never leaves a half-written script.
139
159
  atomic_write(abs_path, expected)
140
160
  click.secho(f" ✔ {rel_path}", fg="green")
161
+ if not filter_active:
162
+ _record_cli_version(product_path)
141
163
 
142
164
  click.echo()
143
165
  click.secho(" Done.", fg="cyan")
@@ -110,9 +110,17 @@ def _run_sat_check(workspace, app_name, data, feature_list, print_config):
110
110
 
111
111
  def _run_compat_check(workspace, product_dir):
112
112
  """Run contract compatibility check. Returns (findings, errors, warnings)."""
113
- from splent_cli.commands.feature.feature_compat import run_all_product_check
113
+ from splent_cli.commands.feature.feature_compat import (
114
+ env_scope_findings,
115
+ naming_findings,
116
+ network_findings,
117
+ run_all_product_check,
118
+ )
114
119
 
115
120
  findings = run_all_product_check(workspace, product_dir)
121
+ findings += env_scope_findings(workspace, product_dir)
122
+ findings += network_findings(workspace, product_dir)
123
+ findings += naming_findings(workspace, product_dir)
116
124
  errors = [f for f in findings if f["severity"] == "error"]
117
125
  warnings = [f for f in findings if f["severity"] == "warning"]
118
126
  return findings, errors, warnings
@@ -21,7 +21,7 @@ something is already wrong:
21
21
 
22
22
  and two more keep what it publishes equal to what the tag says:
23
23
 
24
- * origin/main only ever receives the commit the tag describes, as a
24
+ * the release branch on origin only ever receives the commit the tag describes, as a
25
25
  fast-forward. A divergence leaves the forge half stuck, never a tag on a
26
26
  commit no remote branch contains
27
27
  * nothing is built from uncommitted changes, and the Docker image, whose
@@ -153,25 +153,26 @@ def _is_ancestor(path: str, ancestor: str, descendant: str) -> bool:
153
153
 
154
154
 
155
155
  def plan_main_push(path: str, tag: str) -> tuple[str, str | None]:
156
- """How origin/main comes to carry the commit *tag* describes.
156
+ """How the release branch on origin comes to carry the commit *tag* describes.
157
157
 
158
158
  Returns ``(state, sha)``, where *sha* is the commit that is or will be
159
159
  tagged: the tag's commit when the tag exists locally, otherwise HEAD,
160
160
  because that is where the tag will be created. Never HEAD when a tag
161
161
  exists, since the operator may have committed more work since.
162
162
 
163
- ``contained`` origin/main already has it, nothing to push
164
- ``push`` pushing it is a fast-forward of origin/main
163
+ ``contained`` origin already has it on the branch, nothing to push
164
+ ``push`` pushing it is a fast-forward of that branch
165
165
  ``diverged`` it is neither, so no tag can be published on it
166
- ``unknown`` there is no origin/main to compare with, nothing is pushed
166
+ ``unknown`` origin has no such branch to compare with, nothing is pushed
167
167
  ``timeout`` origin did not answer in time, so nothing can be compared
168
168
  """
169
+ branch = release.release_branch(path)
169
170
  ref = (
170
171
  f"refs/tags/{tag}^{{commit}}" if release.local_tag_exists(path, tag) else "HEAD"
171
172
  )
172
173
  try:
173
174
  subprocess.run(
174
- ["git", "fetch", "origin", "main"],
175
+ ["git", "fetch", "origin", branch],
175
176
  cwd=path,
176
177
  capture_output=True,
177
178
  env=_non_interactive_env(),
@@ -180,7 +181,7 @@ def plan_main_push(path: str, tag: str) -> tuple[str, str | None]:
180
181
  except subprocess.TimeoutExpired:
181
182
  return "timeout", _git_out(path, ["rev-parse", "--verify", ref])
182
183
  sha = _git_out(path, ["rev-parse", "--verify", ref])
183
- remote = _git_out(path, ["rev-parse", "--verify", "refs/remotes/origin/main"])
184
+ remote = _git_out(path, ["rev-parse", "--verify", f"refs/remotes/origin/{branch}"])
184
185
  if not sha or not remote:
185
186
  return "unknown", sha
186
187
  if sha == remote or _is_ancestor(path, sha, remote):
@@ -191,10 +192,11 @@ def plan_main_push(path: str, tag: str) -> tuple[str, str | None]:
191
192
 
192
193
 
193
194
  def push_commit_to_main(path: str, sha: str) -> str | None:
194
- """Fast-forward origin/main to *sha*. None on success, else the reason."""
195
+ """Fast-forward the release branch on origin to *sha*. None on success, else the reason."""
196
+ branch = release.release_branch(path)
195
197
  try:
196
198
  r = subprocess.run(
197
- ["git", "push", "origin", f"{sha}:refs/heads/main"],
199
+ ["git", "push", "origin", f"{sha}:refs/heads/{branch}"],
198
200
  cwd=path,
199
201
  capture_output=True,
200
202
  text=True,
@@ -207,7 +209,7 @@ def push_commit_to_main(path: str, sha: str) -> str | None:
207
209
  )
208
210
  if r.returncode != 0:
209
211
  return (r.stderr or "").strip() or f"git push exited {r.returncode}"
210
- click.echo(f" push origin/main updated to {sha[:12]}")
212
+ click.echo(f" push origin/{branch} updated to {sha[:12]}")
211
213
  return None
212
214
 
213
215
 
@@ -419,7 +421,7 @@ def release_resume(target, assume_yes, no_pypi, no_forge, no_github, no_docker,
419
421
  push_tag = make_release = False
420
422
  main_sha = None
421
423
  summary = (
422
- f"origin/main and the commit {tag} describes have diverged, so "
424
+ f"origin/{release.release_branch(path)} and the commit {tag} describes have diverged, so "
423
425
  "the tag was not pushed"
424
426
  if main_state == "diverged"
425
427
  else "origin did not answer 'git fetch' within "
@@ -522,7 +524,9 @@ def release_resume(target, assume_yes, no_pypi, no_forge, no_github, no_docker,
522
524
 
523
525
  steps = []
524
526
  if main_sha:
525
- steps.append(f"push {main_sha[:12]} to origin/main, the commit {tag} describes")
527
+ steps.append(
528
+ f"push {main_sha[:12]} to origin/{release.release_branch(path)}, the commit {tag} describes"
529
+ )
526
530
  if upload:
527
531
  steps.append(f"build and upload {package} {normalized} to PyPI")
528
532
  if push_tag:
@@ -590,9 +594,9 @@ def release_resume(target, assume_yes, no_pypi, no_forge, no_github, no_docker,
590
594
  stuck.append(
591
595
  release_gate.ChannelStatus(
592
596
  release_gate.FORGE,
593
- summary=f"could not push {main_sha[:12]} to origin/main, {failure}",
597
+ summary=f"could not push {main_sha[:12]} to origin/{release.release_branch(path)}, {failure}",
594
598
  hints=[
595
- "Fix the push to origin/main, then run "
599
+ f"Fix the push to origin/{release.release_branch(path)}, then run "
596
600
  f"'splent release:resume {hint_target}' again.",
597
601
  ],
598
602
  )
@@ -604,7 +608,7 @@ def release_resume(target, assume_yes, no_pypi, no_forge, no_github, no_docker,
604
608
  push_image=push_image,
605
609
  summary=f"not built, there is no {tag} to build it from",
606
610
  hint=f"Nothing is built before {tag} exists. Fix the push "
607
- f"to origin/main, then run 'splent release:resume "
611
+ f"to origin/{release.release_branch(path)}, then run 'splent release:resume "
608
612
  f"{hint_target}' again.",
609
613
  )
610
614
  upload = push_image = False