splent-cli 1.34.1__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.1/src/splent_cli.egg-info → splent_cli-1.35.0}/PKG-INFO +1 -1
  2. {splent_cli-1.34.1 → splent_cli-1.35.0}/pyproject.toml +1 -1
  3. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/cli.py +48 -1
  4. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/feature/feature_compat.py +135 -0
  5. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/feature/feature_create.py +7 -0
  6. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/feature/feature_detach.py +4 -3
  7. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/feature/feature_env.py +4 -1
  8. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/feature/feature_refine.py +7 -0
  9. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/feature/feature_remove.py +5 -4
  10. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/feature/feature_test.py +5 -20
  11. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/product/product_resolve.py +8 -1
  12. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/product/product_sync_template.py +23 -1
  13. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/product/product_validate.py +9 -1
  14. splent_cli-1.35.0/src/splent_cli/services/web_container.py +66 -0
  15. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/templates/product/product_ci_validate.yml.j2 +15 -0
  16. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/utils/cache_utils.py +34 -0
  17. splent_cli-1.35.0/src/splent_cli/utils/feature_naming.py +58 -0
  18. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/utils/integrity.py +2 -2
  19. {splent_cli-1.34.1 → splent_cli-1.35.0/src/splent_cli.egg-info}/PKG-INFO +1 -1
  20. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli.egg-info/SOURCES.txt +2 -0
  21. {splent_cli-1.34.1 → splent_cli-1.35.0}/COPYING +0 -0
  22. {splent_cli-1.34.1 → splent_cli-1.35.0}/COPYING.LESSER +0 -0
  23. {splent_cli-1.34.1 → splent_cli-1.35.0}/README.md +0 -0
  24. {splent_cli-1.34.1 → splent_cli-1.35.0}/setup.cfg +0 -0
  25. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/__init__.py +0 -0
  26. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/__main__.py +0 -0
  27. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/__init__.py +0 -0
  28. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/cache/__init__.py +0 -0
  29. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/cache/cache_clear.py +0 -0
  30. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/cache/cache_orphans.py +0 -0
  31. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/cache/cache_outdated.py +0 -0
  32. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/cache/cache_prune.py +0 -0
  33. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/cache/cache_size.py +0 -0
  34. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/cache/cache_status.py +0 -0
  35. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/cache/cache_usage.py +0 -0
  36. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/cache/cache_versions.py +0 -0
  37. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/check/__init__.py +0 -0
  38. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/check/check_contracts.py +0 -0
  39. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/check/check_deps.py +0 -0
  40. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/check/check_docker.py +0 -0
  41. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/check/check_env.py +0 -0
  42. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/check/check_features.py +0 -0
  43. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/check/check_github.py +0 -0
  44. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/check/check_infra.py +0 -0
  45. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/check/check_product.py +0 -0
  46. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/check/check_pypi.py +0 -0
  47. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/check/check_pyproject.py +0 -0
  48. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/check/check_releases.py +0 -0
  49. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/clear/__init__.py +0 -0
  50. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/clear/clear_build.py +0 -0
  51. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/clear/clear_log.py +0 -0
  52. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/clear/clear_uploads.py +0 -0
  53. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/command/__init__.py +0 -0
  54. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/command/command_create.py +0 -0
  55. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/coverage.py +0 -0
  56. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/database/db_console.py +0 -0
  57. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/database/db_dump.py +0 -0
  58. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/database/db_migrate.py +0 -0
  59. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/database/db_reset.py +0 -0
  60. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/database/db_restore.py +0 -0
  61. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/database/db_rollback.py +0 -0
  62. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/database/db_seed.py +0 -0
  63. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/database/db_status.py +0 -0
  64. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/database/db_upgrade.py +0 -0
  65. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/doctor.py +0 -0
  66. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/env/env_list.py +0 -0
  67. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/env/env_set.py +0 -0
  68. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/env/env_show.py +0 -0
  69. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/export/__init__.py +0 -0
  70. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/export/export_puml.py +0 -0
  71. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/feature/feature_add.py +0 -0
  72. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/feature/feature_attach.py +0 -0
  73. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/feature/feature_clean.py +0 -0
  74. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/feature/feature_clone.py +0 -0
  75. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/feature/feature_compile.py +0 -0
  76. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/feature/feature_contract.py +0 -0
  77. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/feature/feature_delete.py +0 -0
  78. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/feature/feature_discard.py +0 -0
  79. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/feature/feature_drift.py +0 -0
  80. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/feature/feature_fork.py +0 -0
  81. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/feature/feature_git.py +0 -0
  82. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/feature/feature_hook_add.py +0 -0
  83. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/feature/feature_hook_remove.py +0 -0
  84. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/feature/feature_hooks.py +0 -0
  85. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/feature/feature_impact.py +0 -0
  86. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/feature/feature_info.py +0 -0
  87. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/feature/feature_inject_config.py +0 -0
  88. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/feature/feature_install.py +0 -0
  89. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/feature/feature_list.py +0 -0
  90. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/feature/feature_order.py +0 -0
  91. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/feature/feature_outdated.py +0 -0
  92. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/feature/feature_pin.py +0 -0
  93. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/feature/feature_pip_install.py +0 -0
  94. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/feature/feature_pull.py +0 -0
  95. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/feature/feature_release.py +0 -0
  96. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/feature/feature_rename.py +0 -0
  97. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/feature/feature_search.py +0 -0
  98. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/feature/feature_status.py +0 -0
  99. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/feature/feature_sync_template.py +0 -0
  100. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/feature/feature_translate.py +0 -0
  101. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/feature/feature_unlock.py +0 -0
  102. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/feature/feature_upgrade.py +0 -0
  103. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/feature/feature_versions.py +0 -0
  104. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/feature/feature_xray.py +0 -0
  105. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/lint.py +0 -0
  106. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/locust.py +0 -0
  107. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/marketplace/login.py +0 -0
  108. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/marketplace/logout.py +0 -0
  109. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/marketplace/marketplace_auth_utils.py +0 -0
  110. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/marketplace/marketplace_index.py +0 -0
  111. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/marketplace/whoami.py +0 -0
  112. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/product/__init__.py +0 -0
  113. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/product/product_auto_require.py +0 -0
  114. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/product/product_build.py +0 -0
  115. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/product/product_clean.py +0 -0
  116. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/product/product_commands.py +0 -0
  117. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/product/product_config.py +0 -0
  118. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/product/product_configure.py +0 -0
  119. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/product/product_console.py +0 -0
  120. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/product/product_containers.py +0 -0
  121. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/product/product_create.py +0 -0
  122. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/product/product_deploy.py +0 -0
  123. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/product/product_derive.py +0 -0
  124. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/product/product_deselect.py +0 -0
  125. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/product/product_down.py +0 -0
  126. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/product/product_drift.py +0 -0
  127. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/product/product_env.py +0 -0
  128. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/product/product_list.py +0 -0
  129. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/product/product_logs.py +0 -0
  130. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/product/product_missing.py +0 -0
  131. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/product/product_port.py +0 -0
  132. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/product/product_release.py +0 -0
  133. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/product/product_restart.py +0 -0
  134. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/product/product_routes.py +0 -0
  135. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/product/product_run.py +0 -0
  136. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/product/product_select.py +0 -0
  137. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/product/product_shell.py +0 -0
  138. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/product/product_signals.py +0 -0
  139. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/product/product_test.py +0 -0
  140. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/product/product_up.py +0 -0
  141. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/release/__init__.py +0 -0
  142. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/release/release_core.py +0 -0
  143. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/release/release_resume.py +0 -0
  144. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/selenium.py +0 -0
  145. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/spl/__init__.py +0 -0
  146. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/spl/spl_add_constraints.py +0 -0
  147. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/spl/spl_add_feature.py +0 -0
  148. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/spl/spl_compare.py +0 -0
  149. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/spl/spl_configurations.py +0 -0
  150. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/spl/spl_create.py +0 -0
  151. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/spl/spl_deps.py +0 -0
  152. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/spl/spl_features.py +0 -0
  153. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/spl/spl_fetch.py +0 -0
  154. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/spl/spl_info.py +0 -0
  155. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/spl/spl_list.py +0 -0
  156. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/spl/spl_outdated.py +0 -0
  157. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/spl/spl_pin.py +0 -0
  158. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/spl/spl_publish.py +0 -0
  159. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/spl/spl_remove_feature.py +0 -0
  160. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/spl/spl_utils.py +0 -0
  161. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/tokens_setup.py +0 -0
  162. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/uvl/uvl_utils.py +0 -0
  163. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/commands/version.py +0 -0
  164. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/services/__init__.py +0 -0
  165. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/services/compose.py +0 -0
  166. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/services/context.py +0 -0
  167. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/services/credentials.py +0 -0
  168. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/services/deploy_bundle.py +0 -0
  169. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/services/image_registry.py +0 -0
  170. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/services/index_refresh.py +0 -0
  171. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/services/marketplace.py +0 -0
  172. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/services/marketplace_api.py +0 -0
  173. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/services/marketplace_url.py +0 -0
  174. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/services/preflight.py +0 -0
  175. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/services/pypi_quota.py +0 -0
  176. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/services/registry.py +0 -0
  177. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/services/release.py +0 -0
  178. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/services/release_dry_run.py +0 -0
  179. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/services/release_gate.py +0 -0
  180. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/services/spl_store.py +0 -0
  181. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/templates/feature/feature_.gitignore.j2 +0 -0
  182. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/templates/feature/feature_MANIFEST.in.j2 +0 -0
  183. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/templates/feature/feature_README.md.j2 +0 -0
  184. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/templates/feature/feature_commands.py.j2 +0 -0
  185. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/templates/feature/feature_config.py.j2 +0 -0
  186. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/templates/feature/feature_docker-compose.dev.yml.j2 +0 -0
  187. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/templates/feature/feature_docker-compose.prod.yml.j2 +0 -0
  188. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/templates/feature/feature_docker.env.example.j2 +0 -0
  189. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/templates/feature/feature_forms.py.j2 +0 -0
  190. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/templates/feature/feature_hooks.py.j2 +0 -0
  191. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/templates/feature/feature_init.py.j2 +0 -0
  192. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/templates/feature/feature_init_config.py.j2 +0 -0
  193. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/templates/feature/feature_init_light.py.j2 +0 -0
  194. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/templates/feature/feature_init_service.py.j2 +0 -0
  195. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/templates/feature/feature_migrations_alembic.ini.j2 +0 -0
  196. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/templates/feature/feature_migrations_env.py.j2 +0 -0
  197. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/templates/feature/feature_migrations_script.py.mako.j2 +0 -0
  198. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/templates/feature/feature_models.py.j2 +0 -0
  199. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/templates/feature/feature_pyproject.toml.j2 +0 -0
  200. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/templates/feature/feature_pyproject_minimal.toml.j2 +0 -0
  201. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/templates/feature/feature_repositories.py.j2 +0 -0
  202. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/templates/feature/feature_routes.py.j2 +0 -0
  203. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/templates/feature/feature_scripts.js.j2 +0 -0
  204. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/templates/feature/feature_seeders.py.j2 +0 -0
  205. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/templates/feature/feature_services.py.j2 +0 -0
  206. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/templates/feature/feature_signals.py.j2 +0 -0
  207. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/templates/feature/feature_templates_index.html.j2 +0 -0
  208. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/templates/feature/feature_tests_conftest.py.j2 +0 -0
  209. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/templates/feature/feature_tests_e2e.py.j2 +0 -0
  210. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/templates/feature/feature_tests_functional.py.j2 +0 -0
  211. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/templates/feature/feature_tests_integration.py.j2 +0 -0
  212. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/templates/feature/feature_tests_locustfile.py.j2 +0 -0
  213. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/templates/feature/feature_tests_unit.py.j2 +0 -0
  214. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/templates/feature/feature_webpack.config.js.j2 +0 -0
  215. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/templates/licenses/COPYING +0 -0
  216. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/templates/licenses/COPYING.LESSER +0 -0
  217. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/templates/product/product_.env.dev.example.j2 +0 -0
  218. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/templates/product/product_.env.prod.example.j2 +0 -0
  219. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/templates/product/product_.gitignore.j2 +0 -0
  220. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/templates/product/product_00_core_requirements_dev.sh.j2 +0 -0
  221. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/templates/product/product_00_install_features.sh.j2 +0 -0
  222. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/templates/product/product_01_compile_assets.sh.j2 +0 -0
  223. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/templates/product/product_02_0_db_wait_connection.sh.j2 +0 -0
  224. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/templates/product/product_02_1_db_create_db_test.sh.j2 +0 -0
  225. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/templates/product/product_02_2_db_create_splent_migrations.sh.j2 +0 -0
  226. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/templates/product/product_03_initialize_migrations.sh.j2 +0 -0
  227. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/templates/product/product_04_handle_migrations.sh.j2 +0 -0
  228. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/templates/product/product_05_0_start_app_dev.sh.j2 +0 -0
  229. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/templates/product/product_05_1_start_app_prod.sh.j2 +0 -0
  230. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/templates/product/product_400.html +0 -0
  231. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/templates/product/product_401.html +0 -0
  232. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/templates/product/product_404.html +0 -0
  233. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/templates/product/product_500.html +0 -0
  234. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/templates/product/product_Dockerfile.dev.dockerignore.j2 +0 -0
  235. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/templates/product/product_Dockerfile.dev.j2 +0 -0
  236. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/templates/product/product_Dockerfile.prod.dockerignore.j2 +0 -0
  237. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/templates/product/product_Dockerfile.prod.j2 +0 -0
  238. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/templates/product/product_README.md.j2 +0 -0
  239. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/templates/product/product_app.css +0 -0
  240. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/templates/product/product_app.js +0 -0
  241. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/templates/product/product_base_template.html +0 -0
  242. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/templates/product/product_ci_upgrade.yml.j2 +0 -0
  243. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/templates/product/product_config.py.j2 +0 -0
  244. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/templates/product/product_docker-compose.dev.yml.j2 +0 -0
  245. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/templates/product/product_docker-compose.prod.yml.j2 +0 -0
  246. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/templates/product/product_dropzone.css +0 -0
  247. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/templates/product/product_entrypoint.dev.sh.j2 +0 -0
  248. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/templates/product/product_entrypoint.prod.sh.j2 +0 -0
  249. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/templates/product/product_errors.py.j2 +0 -0
  250. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/templates/product/product_favicon.svg +0 -0
  251. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/templates/product/product_init.py.j2 +0 -0
  252. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/templates/product/product_logging.py.j2 +0 -0
  253. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/templates/product/product_own.css +0 -0
  254. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/templates/product/product_package.json.j2 +0 -0
  255. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/templates/product/product_pyproject.toml.j2 +0 -0
  256. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/templates/product/product_uvl.j2 +0 -0
  257. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/utils/__init__.py +0 -0
  258. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/utils/archetype.py +0 -0
  259. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/utils/command_loader.py +0 -0
  260. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/utils/contract_freshness.py +0 -0
  261. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/utils/db_client.py +0 -0
  262. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/utils/db_utils.py +0 -0
  263. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/utils/decorators.py +0 -0
  264. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/utils/dynamic_imports.py +0 -0
  265. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/utils/feature_installer.py +0 -0
  266. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/utils/feature_utils.py +0 -0
  267. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/utils/forge.py +0 -0
  268. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/utils/git_url.py +0 -0
  269. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/utils/io_utils.py +0 -0
  270. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/utils/lifecycle.py +0 -0
  271. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/utils/manifest.py +0 -0
  272. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/utils/path_utils.py +0 -0
  273. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/utils/proc.py +0 -0
  274. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/utils/redact.py +0 -0
  275. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/utils/refinement_facts.py +0 -0
  276. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/utils/running_version.py +0 -0
  277. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/utils/template_drift.py +0 -0
  278. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/utils/toml_edit.py +0 -0
  279. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli/utils/upgrade_plan.py +0 -0
  280. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli.egg-info/dependency_links.txt +0 -0
  281. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli.egg-info/entry_points.txt +0 -0
  282. {splent_cli-1.34.1 → splent_cli-1.35.0}/src/splent_cli.egg-info/requires.txt +0 -0
  283. {splent_cli-1.34.1 → 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.1
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.1"
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,6 +9,7 @@ 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
14
  from splent_cli.utils.running_version import cli_version as _running_cli_version
14
15
 
@@ -223,6 +224,12 @@ def make_feature(full_name, feature_type, docker):
223
224
 
224
225
  # --- Target directory (workspace root for editable features) ---
225
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
+
226
233
  feature_dir = os.path.join(workspace, feature_name)
227
234
  src_path = os.path.join(feature_dir, "src", org_safe, feature_name)
228
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,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
@@ -0,0 +1,66 @@
1
+ """Running a command inside the product's web container.
2
+
3
+ The product's app imports every feature, and therefore every third-party
4
+ package the features declare. Those are installed in the product's image,
5
+ never in the CLI's, which every product on the machine shares. A command
6
+ that builds the app in the CLI container died on the first such import
7
+ (identities needs authlib), so db:upgrade, db:seed and the features' own
8
+ commands only worked for products whose features needed nothing beyond the
9
+ framework. The web container is where the app already runs, with all of it.
10
+ """
11
+
12
+ import os
13
+ import shlex
14
+ import sys
15
+
16
+ from splent_cli.utils.proc import run
17
+
18
+
19
+ def is_product_container() -> bool:
20
+ """Whether this process already runs inside the product's web container.
21
+
22
+ SPLENT_CONTAINER is the explicit answer when set (the CLI container says
23
+ "cli", product images say "product"); /.dockerenv covers containers older
24
+ than the variable.
25
+ """
26
+ container = os.getenv("SPLENT_CONTAINER")
27
+ if container:
28
+ return container != "cli"
29
+ return os.path.exists("/.dockerenv")
30
+
31
+
32
+ def name(product: str) -> str:
33
+ return f"{product}_web"
34
+
35
+
36
+ def running(container: str) -> bool:
37
+ result = run(
38
+ ["docker", "ps", "-q", "-f", f"name=^{container}$"], check=False, capture=True
39
+ )
40
+ return result.returncode == 0 and bool(result.stdout.strip())
41
+
42
+
43
+ def delegate(product: str, argv: list[str]) -> int:
44
+ """Run ``splent <argv>`` in the product's web container; its exit code.
45
+
46
+ With a terminal the exec gets one too, so prompts, colours and Ctrl-C
47
+ behave as they do here.
48
+ """
49
+ command = " ".join(shlex.quote(a) for a in ["splent", *argv])
50
+ tty = ["-t"] if sys.stdin.isatty() and sys.stdout.isatty() else []
51
+ result = run(
52
+ [
53
+ "docker",
54
+ "exec",
55
+ "-i",
56
+ *tty,
57
+ "-e",
58
+ f"SPLENT_APP={product}",
59
+ name(product),
60
+ "bash",
61
+ "-lc",
62
+ command,
63
+ ],
64
+ check=False,
65
+ )
66
+ return result.returncode
@@ -48,3 +48,18 @@ jobs:
48
48
 
49
49
  - name: Generate the deploy artifacts
50
50
  run: splent product:build --no-image
51
+
52
+ # What the build writes is versioned, so a host deploys from a clone.
53
+ # A change to the features or the configuration that skipped
54
+ # product:build left them behind, and the CI said nothing.
55
+ - name: The versioned deploy artifacts are the ones the product generates
56
+ working-directory: {{ product_name }}
57
+ run: |
58
+ paths="deploy docker/requirements.lock splent.contracts.json docker/spl"
59
+ if [ -n "$(git status --porcelain -- $paths)" ]; then
60
+ git status --short -- $paths
61
+ git --no-pager diff --stat -- $paths
62
+ echo "These differ from what splent product:build generates."
63
+ echo "Run splent product:build and commit them."
64
+ exit 1
65
+ fi
@@ -58,6 +58,40 @@ def make_feature_readonly(path: str) -> None:
58
58
  _chmod(os.path.join(root, name), lambda m: m & ~_WRITE_BITS)
59
59
 
60
60
 
61
+ def restore_snapshot(path: str) -> list[str]:
62
+ """Put back what in a cached feature differs from its tag; the paths restored.
63
+
64
+ A snapshot is its tag, read-only, but files can still drift: a CLI that
65
+ copied snapshots without their modes left nginx 1.2.0's entrypoint script
66
+ without its exec bit, and nginx restarted in a loop while every check
67
+ said the release was in place. Untracked files (the docker/.env a product
68
+ generates) are left alone. Only the restored files are made read-only
69
+ again, so that .env stays writable for product:env.
70
+ """
71
+ import subprocess
72
+
73
+ if not os.path.isdir(os.path.join(path, ".git")):
74
+ return []
75
+ status = subprocess.run(
76
+ ["git", "-C", path, "status", "--porcelain", "--untracked-files=no"],
77
+ capture_output=True,
78
+ text=True,
79
+ )
80
+ if status.returncode != 0:
81
+ return []
82
+ changed = [line[3:] for line in status.stdout.splitlines() if line.strip()]
83
+ if not changed:
84
+ return []
85
+ subprocess.run(
86
+ ["git", "-C", path, "checkout", "--", *changed],
87
+ capture_output=True,
88
+ text=True,
89
+ )
90
+ for rel in changed:
91
+ _chmod(os.path.join(path, rel), lambda m: m & ~_WRITE_BITS)
92
+ return changed
93
+
94
+
61
95
  def make_feature_writable(path: str) -> None:
62
96
  """Restore write permissions across a cached feature so it can be edited
63
97
  or deleted.
@@ -0,0 +1,58 @@
1
+ """A feature is named for what it does, never for the product that uses it.
2
+
3
+ auth_uvlhub, signup_uvlhub, profile_planner... each holds what one product
4
+ wanted from a base feature: a slot, a setting, its own markup, its texts.
5
+ No other product can choose it, and while it exists the base never grows the
6
+ extension point every product needed. What a product alone wants belongs in
7
+ the product (its templates, seeders and configuration); what several want
8
+ belongs in the base, as a slot, a registry or a setting.
9
+ """
10
+
11
+ from pathlib import Path
12
+
13
+ from splent_cli.utils.template_drift import not_a_product_reason
14
+
15
+ # Words a product's name carries that say nothing about which product it is.
16
+ GENERIC_WORDS = {"app", "splent", "product", "site", "web"}
17
+
18
+
19
+ def product_brands(workspace: str | Path) -> dict[str, str]:
20
+ """``{brand: product}`` for the products at the workspace root.
21
+
22
+ The brand is the product's name without its generic words: uvlhub for
23
+ uvlhub_splent, innosoft for innosoft_app, egc for egc_wiki.
24
+ """
25
+ brands: dict[str, str] = {}
26
+ root = Path(workspace)
27
+ if not root.is_dir():
28
+ return brands
29
+ for entry in root.iterdir():
30
+ if not entry.is_dir() or entry.name.startswith((".", "splent_")):
31
+ continue
32
+ if not_a_product_reason(entry) is not None:
33
+ continue
34
+ words = [w for w in entry.name.split("_") if w and w not in GENERIC_WORDS]
35
+ if words:
36
+ brands[words[0]] = entry.name
37
+ return brands
38
+
39
+
40
+ def product_suffix(feature_name: str, workspace: str | Path) -> str | None:
41
+ """The product a feature's name ends with (``auth_uvlhub``: uvlhub_splent),
42
+ or None. A name that is the brand alone (``marketplace``) is a domain."""
43
+ short = feature_name.removeprefix("splent_feature_")
44
+ head, sep, last = short.rpartition("_")
45
+ if not sep or not head:
46
+ return None
47
+ return product_brands(workspace).get(last)
48
+
49
+
50
+ def product_suffix_message(feature_name: str, product: str) -> str:
51
+ short = feature_name.removeprefix("splent_feature_")
52
+ return (
53
+ f"{short} is named after the product {product}. A feature is named for "
54
+ "what it does, so another product can choose it. What only this "
55
+ "product wants goes in the product (templates, seeders, "
56
+ "[tool.splent.config]); what several want goes in the base feature, "
57
+ "as a slot, a registry or a setting."
58
+ )