splent-cli 1.24.0__tar.gz → 1.25.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 (196) hide show
  1. {splent_cli-1.24.0/src/splent_cli.egg-info → splent_cli-1.25.0}/PKG-INFO +2 -2
  2. {splent_cli-1.24.0 → splent_cli-1.25.0}/pyproject.toml +2 -2
  3. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/commands/check/check_features.py +11 -3
  4. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/commands/check/check_github.py +28 -14
  5. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/commands/check/check_releases.py +16 -12
  6. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/commands/database/db_migrate.py +18 -0
  7. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/commands/database/db_reset.py +1 -0
  8. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/commands/database/db_rollback.py +1 -0
  9. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/commands/database/db_seed.py +1 -0
  10. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/commands/doctor.py +4 -4
  11. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/commands/env/env_set.py +20 -9
  12. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/commands/feature/feature_attach.py +14 -0
  13. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/commands/feature/feature_contract.py +3 -0
  14. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/commands/feature/feature_create.py +4 -0
  15. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/commands/feature/feature_detach.py +3 -0
  16. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/commands/feature/feature_fork.py +3 -2
  17. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/commands/feature/feature_pip_install.py +14 -4
  18. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/commands/feature/feature_refine.py +10 -1
  19. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/commands/feature/feature_release.py +187 -20
  20. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/commands/feature/feature_remove.py +4 -3
  21. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/commands/feature/feature_rename.py +5 -2
  22. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/commands/feature/feature_test.py +12 -0
  23. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/commands/product/product_build.py +16 -8
  24. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/commands/product/product_create.py +1 -0
  25. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/commands/product/product_deploy.py +13 -10
  26. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/commands/product/product_drift.py +8 -0
  27. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/commands/product/product_env.py +31 -5
  28. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/commands/product/product_release.py +8 -8
  29. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/commands/product/product_run.py +59 -0
  30. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/commands/product/product_sync_template.py +9 -1
  31. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/commands/release/release_core.py +13 -11
  32. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/commands/release/release_resume.py +13 -12
  33. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/commands/spl/spl_add_constraints.py +2 -1
  34. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/commands/spl/spl_add_feature.py +6 -1
  35. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/commands/spl/spl_remove_feature.py +2 -1
  36. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/commands/spl/spl_utils.py +34 -0
  37. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/commands/tokens_setup.py +13 -8
  38. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/services/context.py +35 -0
  39. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/services/preflight.py +6 -2
  40. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/services/release.py +151 -41
  41. splent_cli-1.25.0/src/splent_cli/services/release_dry_run.py +563 -0
  42. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/services/release_gate.py +42 -26
  43. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/services/spl_store.py +53 -0
  44. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/utils/contract_freshness.py +27 -4
  45. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/utils/dynamic_imports.py +50 -5
  46. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/utils/feature_utils.py +63 -0
  47. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/utils/forge.py +14 -0
  48. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/utils/io_utils.py +86 -1
  49. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/utils/template_drift.py +1 -0
  50. {splent_cli-1.24.0 → splent_cli-1.25.0/src/splent_cli.egg-info}/PKG-INFO +2 -2
  51. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli.egg-info/SOURCES.txt +1 -0
  52. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli.egg-info/requires.txt +1 -1
  53. {splent_cli-1.24.0 → splent_cli-1.25.0}/LICENSE +0 -0
  54. {splent_cli-1.24.0 → splent_cli-1.25.0}/README.md +0 -0
  55. {splent_cli-1.24.0 → splent_cli-1.25.0}/setup.cfg +0 -0
  56. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/__init__.py +0 -0
  57. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/__main__.py +0 -0
  58. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/cli.py +0 -0
  59. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/commands/__init__.py +0 -0
  60. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/commands/cache/__init__.py +0 -0
  61. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/commands/cache/cache_clear.py +0 -0
  62. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/commands/cache/cache_orphans.py +0 -0
  63. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/commands/cache/cache_outdated.py +0 -0
  64. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/commands/cache/cache_prune.py +0 -0
  65. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/commands/cache/cache_size.py +0 -0
  66. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/commands/cache/cache_status.py +0 -0
  67. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/commands/cache/cache_usage.py +0 -0
  68. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/commands/cache/cache_versions.py +0 -0
  69. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/commands/check/__init__.py +0 -0
  70. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/commands/check/check_contracts.py +0 -0
  71. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/commands/check/check_deps.py +0 -0
  72. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/commands/check/check_docker.py +0 -0
  73. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/commands/check/check_env.py +0 -0
  74. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/commands/check/check_infra.py +0 -0
  75. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/commands/check/check_product.py +0 -0
  76. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/commands/check/check_pypi.py +0 -0
  77. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/commands/check/check_pyproject.py +0 -0
  78. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/commands/clear/__init__.py +0 -0
  79. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/commands/clear/clear_build.py +0 -0
  80. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/commands/clear/clear_log.py +0 -0
  81. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/commands/clear/clear_uploads.py +0 -0
  82. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/commands/command/__init__.py +0 -0
  83. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/commands/command/command_create.py +0 -0
  84. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/commands/coverage.py +0 -0
  85. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/commands/database/db_console.py +0 -0
  86. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/commands/database/db_dump.py +0 -0
  87. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/commands/database/db_restore.py +0 -0
  88. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/commands/database/db_status.py +0 -0
  89. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/commands/database/db_upgrade.py +0 -0
  90. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/commands/env/env_list.py +0 -0
  91. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/commands/env/env_show.py +0 -0
  92. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/commands/export/__init__.py +0 -0
  93. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/commands/export/export_puml.py +0 -0
  94. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/commands/feature/feature_add.py +0 -0
  95. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/commands/feature/feature_clean.py +0 -0
  96. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/commands/feature/feature_clone.py +0 -0
  97. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/commands/feature/feature_compat.py +0 -0
  98. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/commands/feature/feature_compile.py +0 -0
  99. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/commands/feature/feature_delete.py +0 -0
  100. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/commands/feature/feature_discard.py +0 -0
  101. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/commands/feature/feature_drift.py +0 -0
  102. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/commands/feature/feature_env.py +0 -0
  103. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/commands/feature/feature_git.py +0 -0
  104. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/commands/feature/feature_hook_add.py +0 -0
  105. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/commands/feature/feature_hook_remove.py +0 -0
  106. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/commands/feature/feature_hooks.py +0 -0
  107. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/commands/feature/feature_impact.py +0 -0
  108. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/commands/feature/feature_info.py +0 -0
  109. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/commands/feature/feature_inject_config.py +0 -0
  110. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/commands/feature/feature_install.py +0 -0
  111. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/commands/feature/feature_list.py +0 -0
  112. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/commands/feature/feature_order.py +0 -0
  113. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/commands/feature/feature_outdated.py +0 -0
  114. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/commands/feature/feature_pin.py +0 -0
  115. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/commands/feature/feature_pull.py +0 -0
  116. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/commands/feature/feature_search.py +0 -0
  117. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/commands/feature/feature_status.py +0 -0
  118. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/commands/feature/feature_sync_template.py +0 -0
  119. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/commands/feature/feature_translate.py +0 -0
  120. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/commands/feature/feature_unlock.py +0 -0
  121. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/commands/feature/feature_upgrade.py +0 -0
  122. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/commands/feature/feature_versions.py +0 -0
  123. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/commands/feature/feature_xray.py +0 -0
  124. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/commands/lint.py +0 -0
  125. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/commands/locust.py +0 -0
  126. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/commands/marketplace/login.py +0 -0
  127. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/commands/marketplace/logout.py +0 -0
  128. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/commands/marketplace/marketplace_auth_utils.py +0 -0
  129. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/commands/marketplace/marketplace_index.py +0 -0
  130. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/commands/marketplace/whoami.py +0 -0
  131. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/commands/product/__init__.py +0 -0
  132. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/commands/product/product_auto_require.py +0 -0
  133. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/commands/product/product_clean.py +0 -0
  134. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/commands/product/product_commands.py +0 -0
  135. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/commands/product/product_config.py +0 -0
  136. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/commands/product/product_configure.py +0 -0
  137. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/commands/product/product_console.py +0 -0
  138. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/commands/product/product_containers.py +0 -0
  139. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/commands/product/product_derive.py +0 -0
  140. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/commands/product/product_deselect.py +0 -0
  141. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/commands/product/product_down.py +0 -0
  142. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/commands/product/product_list.py +0 -0
  143. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/commands/product/product_logs.py +0 -0
  144. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/commands/product/product_missing.py +0 -0
  145. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/commands/product/product_port.py +0 -0
  146. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/commands/product/product_resolve.py +0 -0
  147. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/commands/product/product_restart.py +0 -0
  148. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/commands/product/product_routes.py +0 -0
  149. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/commands/product/product_select.py +0 -0
  150. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/commands/product/product_shell.py +0 -0
  151. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/commands/product/product_signals.py +0 -0
  152. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/commands/product/product_test.py +0 -0
  153. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/commands/product/product_up.py +0 -0
  154. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/commands/product/product_validate.py +0 -0
  155. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/commands/release/__init__.py +0 -0
  156. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/commands/selenium.py +0 -0
  157. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/commands/spl/__init__.py +0 -0
  158. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/commands/spl/spl_compare.py +0 -0
  159. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/commands/spl/spl_configurations.py +0 -0
  160. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/commands/spl/spl_create.py +0 -0
  161. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/commands/spl/spl_deps.py +0 -0
  162. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/commands/spl/spl_features.py +0 -0
  163. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/commands/spl/spl_fetch.py +0 -0
  164. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/commands/spl/spl_info.py +0 -0
  165. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/commands/spl/spl_list.py +0 -0
  166. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/commands/spl/spl_migrate_catalog.py +0 -0
  167. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/commands/spl/spl_outdated.py +0 -0
  168. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/commands/spl/spl_pin.py +0 -0
  169. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/commands/spl/spl_publish.py +0 -0
  170. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/commands/uvl/uvl_utils.py +0 -0
  171. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/commands/version.py +0 -0
  172. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/services/__init__.py +0 -0
  173. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/services/compose.py +0 -0
  174. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/services/credentials.py +0 -0
  175. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/services/index_refresh.py +0 -0
  176. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/services/marketplace.py +0 -0
  177. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/services/marketplace_api.py +0 -0
  178. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/services/marketplace_url.py +0 -0
  179. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/services/pypi_quota.py +0 -0
  180. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/services/registry.py +0 -0
  181. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/utils/__init__.py +0 -0
  182. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/utils/archetype.py +0 -0
  183. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/utils/cache_utils.py +0 -0
  184. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/utils/command_loader.py +0 -0
  185. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/utils/db_utils.py +0 -0
  186. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/utils/decorators.py +0 -0
  187. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/utils/feature_installer.py +0 -0
  188. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/utils/git_url.py +0 -0
  189. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/utils/integrity.py +0 -0
  190. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/utils/lifecycle.py +0 -0
  191. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/utils/manifest.py +0 -0
  192. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/utils/path_utils.py +0 -0
  193. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli/utils/proc.py +0 -0
  194. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli.egg-info/dependency_links.txt +0 -0
  195. {splent_cli-1.24.0 → splent_cli-1.25.0}/src/splent_cli.egg-info/entry_points.txt +0 -0
  196. {splent_cli-1.24.0 → splent_cli-1.25.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.24.0
3
+ Version: 1.25.0
4
4
  Summary: SPLENT-CLI is a CLI to be able to work on your development more easily.
5
5
  Author-email: DiversoLab <diversolab@us.es>
6
6
  Project-URL: Homepage, https://github.com/diverso-lab/splent_cli
@@ -14,7 +14,7 @@ Requires-Dist: packaging>=24.0
14
14
  Requires-Dist: python-dotenv>=1.0
15
15
  Requires-Dist: pyyaml>=6.0
16
16
  Requires-Dist: requests>=2.31.0
17
- Requires-Dist: splent_framework<2,>=1.15.0
17
+ Requires-Dist: splent_framework<2,>=1.15.1
18
18
  Requires-Dist: tomli_w>=1.0.0
19
19
  Provides-Extra: dev
20
20
  Requires-Dist: setuptools==80.3.1; extra == "dev"
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "splent_cli"
7
- version = "1.24.0"
7
+ version = "1.25.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"
@@ -24,7 +24,7 @@ dependencies = [
24
24
  "requests>=2.31.0",
25
25
  # The CLI drives the framework (paths, feature order, migrations, the
26
26
  # app factory) and only works with the same major version of it.
27
- "splent_framework>=1.15.0,<2",
27
+ "splent_framework>=1.15.1,<2",
28
28
  "tomli_w>=1.0.0",
29
29
  ]
30
30
 
@@ -11,6 +11,8 @@ import tomllib
11
11
 
12
12
  from splent_cli.services import context
13
13
  from splent_cli.utils.feature_utils import normalize_namespace, read_features_from_data
14
+ from splent_cli.utils.lifecycle import state_rank
15
+ from splent_cli.utils.manifest import feature_key, get_feature_state
14
16
 
15
17
 
16
18
  def _pkg_installed(name: str) -> bool:
@@ -125,11 +127,17 @@ def check_features():
125
127
  else:
126
128
  _fail("No symlink in product/features/")
127
129
 
128
- # 3. Pip install
129
- if _pkg_installed(name):
130
+ # 3. Pip install. Features are installed in the product's web
131
+ # container, not in the CLI's, so the manifest the install script
132
+ # updates is what says so. Checking this interpreter alone flagged
133
+ # every feature of a running product as not installed.
134
+ state = get_feature_state(product_path, feature_key(org_safe, name, version))
135
+ if state_rank(state) >= state_rank("installed"):
136
+ _ok(f"Installed in the product container ({state})")
137
+ elif _pkg_installed(name):
130
138
  _ok("pip installed")
131
139
  else:
132
- _warn("Not pip-installed (run product:run to install)")
140
+ _warn("Not installed yet (product:derive installs it)")
133
141
 
134
142
  # 4. Git state (editable only)
135
143
  if not version:
@@ -1,4 +1,3 @@
1
- import os
2
1
  import urllib.request
3
2
  import urllib.error
4
3
  import json
@@ -7,32 +6,29 @@ import click
7
6
  from splent_cli.utils import forge
8
7
 
9
8
 
10
- @click.command(
11
- "check:github",
12
- short_help="Verify forge credentials from .env (GITHUB_USER / forge token)",
13
- )
14
- def check_github():
15
- # The command keeps its name, but the forge it checks is whatever the
16
- # workspace is configured against: github.com by default, a self-hosted
17
- # Forgejo when SPLENT_FORGE_HOST says so.
9
+ def _check_forge():
10
+ # The forge checked is whatever the workspace is configured against:
11
+ # git.splent.io by default, another Forgejo, Gitea or github.com when
12
+ # SPLENT_FORGE_HOST says so.
18
13
  forge_host = forge.host()
19
14
  token_var = forge.token_env_name()
15
+ user_var = forge.user_env_name()
20
16
 
21
17
  click.echo(click.style(f"\n{forge_host} Credentials Check\n", fg="cyan", bold=True))
22
18
 
23
- user = os.getenv("GITHUB_USER", "").strip()
19
+ user = (forge.user() or "").strip()
24
20
  # Resolved through forge so a self-hosted forge reads SPLENT_FORGE_TOKEN
25
21
  # while GitHub keeps reading GITHUB_TOKEN, as it always has.
26
22
  token = (forge.token() or "").strip()
27
23
 
28
24
  # --- presence checks ---
29
25
  if not user:
30
- click.echo(click.style("[✖] ", fg="red") + "GITHUB_USER not set in .env")
26
+ click.echo(click.style("[✖] ", fg="red") + f"{user_var} not set in .env")
31
27
  click.secho(
32
28
  " Run 'splent tokens:setup' for setup instructions.", fg="bright_black"
33
29
  )
34
30
  raise SystemExit(1)
35
- click.echo(click.style("[✔] ", fg="green") + f"GITHUB_USER = {user}")
31
+ click.echo(click.style("[✔] ", fg="green") + f"{user_var} = {user}")
36
32
 
37
33
  if not token:
38
34
  click.echo(click.style("[✖] ", fg="red") + f"{token_var} not set in .env")
@@ -98,7 +94,7 @@ def check_github():
98
94
  if api_login.lower() != user.lower():
99
95
  click.echo(
100
96
  click.style("[⚠] ", fg="yellow")
101
- + f"Token belongs to '{api_login}', but GITHUB_USER is '{user}'"
97
+ + f"Token belongs to '{api_login}', but {user_var} is '{user}'"
102
98
  )
103
99
  else:
104
100
  click.echo(click.style("[✔] ", fg="green") + f"Authenticated as {api_login}")
@@ -123,4 +119,22 @@ def check_github():
123
119
  click.secho(f"Credentials for {forge_host} OK.", fg="green")
124
120
 
125
121
 
126
- cli_command = check_github
122
+ @click.command(
123
+ "check:forge",
124
+ short_help="Verify the git forge credentials (SPLENT_FORGE_USER / token).",
125
+ )
126
+ def check_forge():
127
+ """Check that the forge user and token in .env work against the forge's
128
+ API (git.splent.io unless SPLENT_FORGE_HOST says otherwise)."""
129
+ _check_forge()
130
+
131
+
132
+ # The name it had while the features lived on github.com. Kept so scripts and
133
+ # older docs keep working, but out of the help listing.
134
+ @click.command("check:github", hidden=True)
135
+ def check_github():
136
+ """Alias of check:forge."""
137
+ _check_forge()
138
+
139
+
140
+ cli_commands = [check_forge, check_github]
@@ -1,5 +1,5 @@
1
1
  """
2
- check:releases — Report GitHub/PyPI divergence across the workspace.
2
+ check:releases — Report forge/PyPI divergence across the workspace.
3
3
 
4
4
  Answers, without anyone having to read a published index by hand, the two
5
5
  questions a two-channel release can go wrong in:
@@ -28,6 +28,7 @@ import click
28
28
 
29
29
  from splent_cli.services import context, registry, release_gate
30
30
  from splent_cli.services.release import extract_repo, read_project_name
31
+ from splent_cli.utils import forge
31
32
 
32
33
 
33
34
  CORE_PACKAGES = ("splent_cli", "splent_framework")
@@ -142,7 +143,7 @@ def discover_packages(
142
143
 
143
144
  @click.command(
144
145
  "check:releases",
145
- short_help="Report GitHub/PyPI divergence for every package in the workspace.",
146
+ short_help="Report forge/PyPI divergence for every package in the workspace.",
146
147
  )
147
148
  @click.option("--feature", "only", default=None, help="Check one package by name.")
148
149
  @click.option(
@@ -168,7 +169,7 @@ def discover_packages(
168
169
  def check_releases(only, include_core, include_products, divergent_only):
169
170
  """Compare git tags against PyPI releases for the whole workspace."""
170
171
  workspace = str(context.workspace())
171
- token = os.getenv("GITHUB_TOKEN")
172
+ token = forge.token()
172
173
 
173
174
  packages = discover_packages(
174
175
  workspace,
@@ -224,12 +225,12 @@ def check_releases(only, include_core, include_products, divergent_only):
224
225
 
225
226
  org, repo_name = repo.split("/", 1)
226
227
  wants_pypi = release_gate.PYPI in channels
227
- wants_github = release_gate.GITHUB in channels
228
+ wants_github = release_gate.FORGE in channels
228
229
 
229
230
  try:
230
231
  tags = registry.list_semver_tags(org, repo_name, token, quiet=False)
231
232
  except registry.RegistryError as e:
232
- reason = _reason(e, "GitHub")
233
+ reason = _reason(e, "the forge")
233
234
  unanswered.append((name, reason))
234
235
  click.echo(click.style(" [!] ", fg="yellow") + f"{name}: {reason}")
235
236
  continue
@@ -249,7 +250,7 @@ def check_releases(only, include_core, include_products, divergent_only):
249
250
  try:
250
251
  release_tags = registry.list_release_tags(org, repo_name, token)
251
252
  except registry.RegistryError as e:
252
- reason = _reason(e, "GitHub") + " when listing releases"
253
+ reason = _reason(e, "the forge") + " when listing releases"
253
254
  unanswered.append((name, reason))
254
255
  click.echo(click.style(" [!] ", fg="yellow") + f"{name}: {reason}")
255
256
  continue
@@ -277,7 +278,7 @@ def check_releases(only, include_core, include_products, divergent_only):
277
278
  if missing:
278
279
  tag_without_release.append((name, missing))
279
280
  problems.append(
280
- f"{len(missing)} tag(s) with no GitHub release: "
281
+ f"{len(missing)} tag(s) with no release on the forge: "
281
282
  + ", ".join(missing[:6])
282
283
  + (" ..." if len(missing) > 6 else "")
283
284
  )
@@ -290,7 +291,7 @@ def check_releases(only, include_core, include_products, divergent_only):
290
291
  if not wants_pypi:
291
292
  click.echo(
292
293
  click.style(" [OK] ", fg="green")
293
- + f"{name}: github only by declaration, {len(tags)} tag(s)"
294
+ + f"{name}: forge only by declaration, {len(tags)} tag(s)"
294
295
  )
295
296
  elif not tags and not pypi_versions:
296
297
  click.echo(
@@ -310,7 +311,8 @@ def check_releases(only, include_core, include_products, divergent_only):
310
311
  if not total and not unanswered:
311
312
  checked = len(packages) - len(no_remote)
312
313
  click.secho(
313
- f" GitHub and PyPI agree for all {checked} package(s) checked.", fg="green"
314
+ f" The forge and PyPI agree for all {checked} package(s) checked.",
315
+ fg="green",
314
316
  )
315
317
  if no_remote:
316
318
  click.secho(
@@ -320,7 +322,8 @@ def check_releases(only, include_core, include_products, divergent_only):
320
322
  click.echo()
321
323
  if not token:
322
324
  click.secho(
323
- " Set GITHUB_TOKEN to avoid rate limits on this command.", fg="yellow"
325
+ f" Set {forge.token_env_name()} to avoid rate limits on this command.",
326
+ fg="yellow",
324
327
  )
325
328
  click.echo()
326
329
  return
@@ -358,7 +361,7 @@ def check_releases(only, include_core, include_products, divergent_only):
358
361
 
359
362
  if tag_without_release:
360
363
  click.secho(
361
- f" {len(tag_without_release)} package(s) tagged with no GitHub release",
364
+ f" {len(tag_without_release)} package(s) tagged with no release on the forge",
362
365
  fg="red",
363
366
  bold=True,
364
367
  )
@@ -384,7 +387,8 @@ def check_releases(only, include_core, include_products, divergent_only):
384
387
 
385
388
  if not token:
386
389
  click.secho(
387
- " Set GITHUB_TOKEN to avoid rate limits on this command.", fg="yellow"
390
+ f" Set {forge.token_env_name()} to avoid rate limits on this command.",
391
+ fg="yellow",
388
392
  )
389
393
  click.echo()
390
394
 
@@ -61,6 +61,22 @@ def _format_migration(path: str) -> None:
61
61
  subprocess.run([ruff, "format", "--quiet", path], check=False)
62
62
 
63
63
 
64
+ def _hand_to_directory_owner(path: str) -> None:
65
+ """Give a generated migration the owner of the directory it lands in.
66
+
67
+ The CLI runs as root inside its container, so Alembic wrote the file as
68
+ root:root 0644. On the host that is a file the user cannot save, and a
69
+ blank ``--empty`` migration exists precisely to be edited by hand.
70
+ """
71
+ if not (hasattr(os, "geteuid") and os.geteuid() == 0):
72
+ return
73
+ try:
74
+ st = os.stat(os.path.dirname(path))
75
+ os.chown(path, st.st_uid, st.st_gid)
76
+ except OSError:
77
+ pass
78
+
79
+
64
80
  @requires_db
65
81
  @click.command(
66
82
  "db:migrate",
@@ -190,6 +206,7 @@ def db_migrate(feature, message, empty):
190
206
  if empty:
191
207
  if newest:
192
208
  _format_migration(newest)
209
+ _hand_to_directory_owner(newest)
193
210
  click.echo(
194
211
  click.style(
195
212
  f" 📝 {feat}: blank migration generated — "
@@ -208,6 +225,7 @@ def db_migrate(feature, message, empty):
208
225
  click.echo(click.style(f" ✔ {feat}: up to date", fg="green"))
209
226
  else:
210
227
  _format_migration(newest)
228
+ _hand_to_directory_owner(newest)
211
229
  click.echo(
212
230
  click.style(f" 📝 {feat}: new migration generated", fg="cyan")
213
231
  )
@@ -114,6 +114,7 @@ def _entry_lookup() -> dict:
114
114
  @click.argument("feature_name", required=False)
115
115
  @click.option("-y", "--yes", is_flag=True, help="Confirm without prompting.")
116
116
  @context.requires_product
117
+ @context.refuses_production
117
118
  def db_reset(feature_name, yes):
118
119
  """Reset the database.
119
120
 
@@ -69,6 +69,7 @@ def _find_dependents(feature: str, product_dir: str) -> list[str]:
69
69
  )
70
70
  @click.option("--cascade", is_flag=True, help="Also rollback dependent features.")
71
71
  @context.requires_product
72
+ @context.refuses_production
72
73
  def db_rollback(feature, steps, cascade):
73
74
  app = current_app
74
75
  product_path = PathUtils.get_app_base_dir()
@@ -177,6 +177,7 @@ def _truncate_data():
177
177
  @click.option("-y", "--yes", is_flag=True, help="Skip confirmation prompts.")
178
178
  @click.argument("module", required=False)
179
179
  @context.requires_product
180
+ @context.refuses_production
180
181
  def db_seed(reset, yes, module):
181
182
  if reset:
182
183
  if yes or click.confirm(
@@ -12,7 +12,7 @@ from splent_cli.commands.check.check_env import check_env
12
12
  from splent_cli.commands.check.check_pyproject import check_pyproject
13
13
  from splent_cli.commands.check.check_features import check_features
14
14
  from splent_cli.commands.check.check_docker import check_docker
15
- from splent_cli.commands.check.check_github import check_github
15
+ from splent_cli.commands.check.check_github import check_forge
16
16
  from splent_cli.commands.check.check_pypi import check_pypi
17
17
  from splent_cli.commands.check.check_infra import check_infra
18
18
  from splent_cli.commands.check.check_product import check_product
@@ -49,7 +49,7 @@ SECTIONS = [
49
49
  (
50
50
  "Credentials",
51
51
  [
52
- ("check:github", check_github, False, True),
52
+ ("check:forge", check_forge, False, True),
53
53
  ("check:pypi", check_pypi, False, True),
54
54
  ],
55
55
  ),
@@ -110,10 +110,10 @@ def doctor(fast):
110
110
  Environment — Python, env vars, pyproject, Docker, infrastructure
111
111
  Product — product health, feature cache/symlinks, feature status
112
112
  Services — containers, database migrations
113
- Credentials — GitHub and PyPI tokens (skipped with --fast)
113
+ Credentials — git forge and PyPI tokens (skipped with --fast)
114
114
 
115
115
  \b
116
- Use --fast to skip network checks (GitHub, PyPI) and database checks.
116
+ Use --fast to skip network checks (git forge, PyPI) and database checks.
117
117
 
118
118
  \b
119
119
  For full product validation (UVL + contracts + imports), run:
@@ -1,5 +1,6 @@
1
1
  import click
2
2
  from splent_cli.services import context
3
+ from splent_cli.utils import forge
3
4
 
4
5
 
5
6
  # -------------------------
@@ -67,16 +68,19 @@ def set_mode_interactive():
67
68
 
68
69
 
69
70
  def set_github_interactive():
70
- user = click.prompt("GitHub username", type=str)
71
- token = click.prompt("GitHub personal access token", hide_input=True)
71
+ """Ask for the forge user and token and store them under the variables
72
+ this forge reads (SPLENT_FORGE_* on git.splent.io, GITHUB_* on github.com)."""
73
+ host = forge.host()
74
+ user = click.prompt(f"{host} username", type=str)
75
+ token = click.prompt(f"{host} access token", hide_input=True)
72
76
 
73
- user = _validate_env_value(user, "GitHub username")
74
- token = _validate_env_value(token, "GitHub token")
77
+ user = _validate_env_value(user, f"{host} username")
78
+ token = _validate_env_value(token, f"{host} token")
75
79
 
76
- set_var("GITHUB_USER", user)
77
- set_var("GITHUB_TOKEN", token)
80
+ set_var(forge.user_env_name(), user)
81
+ set_var(forge.token_env_name(), token)
78
82
 
79
- click.secho("✔ GitHub configuration updated.", fg="green")
83
+ click.secho(f"✔ {host} configuration updated.", fg="green")
80
84
  remind_source()
81
85
 
82
86
 
@@ -124,8 +128,15 @@ def env_set_mode():
124
128
  set_mode_interactive()
125
129
 
126
130
 
127
- @env_set_group.command("github")
131
+ @env_set_group.command("forge")
132
+ def env_set_forge():
133
+ """Set the git forge user and token."""
134
+ set_github_interactive()
135
+
136
+
137
+ @env_set_group.command("github", hidden=True)
128
138
  def env_set_github():
139
+ """Alias of `env:set forge`."""
129
140
  set_github_interactive()
130
141
 
131
142
 
@@ -143,7 +154,7 @@ def run_wizard():
143
154
  while True:
144
155
  click.echo("\n=== SPLENT ENV CONFIG WIZARD ===")
145
156
  click.echo("1. Set mode (dev/prod)")
146
- click.echo("2. Configure GitHub credentials")
157
+ click.echo("2. Configure git forge credentials")
147
158
  click.echo("3. Configure PyPI token")
148
159
  click.echo("4. Exit")
149
160
  click.echo("--------------------------------")
@@ -10,7 +10,9 @@ from splent_cli.utils.feature_utils import (
10
10
  product_namespace_spelling,
11
11
  prune_feature_links,
12
12
  read_feature_list,
13
+ read_project_name,
13
14
  remove_feature_link,
15
+ set_feature_distribution,
14
16
  write_features_to_data,
15
17
  )
16
18
  from splent_cli.utils.io_utils import load_toml, atomic_write
@@ -147,6 +149,18 @@ def feature_attach(feature_identifier, version, env_scope):
147
149
  scope_label = f" ({env_scope} only)" if env_scope else ""
148
150
  click.echo(f" {short}@{version} attached{scope_label}")
149
151
 
152
+ # ── PyPI name, when it is not the feature's own ──────────────────
153
+ # The production image installs pinned features from PyPI, so it has to
154
+ # know the name this version was published under. A release under a name
155
+ # of one's own (splent-feature-notes-alice) keeps the import package, and
156
+ # this pin, as splent_feature_notes.
157
+ distribution = read_project_name(versioned_dir)
158
+ data = load_toml(pyproject_path, what="pyproject.toml")
159
+ if set_feature_distribution(data, feature_name, distribution):
160
+ atomic_write(pyproject_path, tomli_w.dumps(data))
161
+ if distribution and distribution != feature_name:
162
+ click.echo(f" pypi {short} is published on PyPI as {distribution}")
163
+
150
164
  # ── Create/update symlink ─────────────────────────────────────────
151
165
  product_features_dir = os.path.join(product_path, "features", namespace_fs)
152
166
  os.makedirs(product_features_dir, exist_ok=True)
@@ -22,6 +22,7 @@ from pathlib import Path
22
22
  from splent_cli.services import context
23
23
  from splent_cli.commands.feature.feature_release import infer_contract, write_contract
24
24
  from splent_cli.utils.feature_utils import normalize_namespace
25
+ from splent_cli.utils.contract_freshness import mark_contract_fresh
25
26
  from splent_cli.utils.io_utils import load_toml
26
27
 
27
28
 
@@ -351,6 +352,8 @@ def feature_contract(feature_ref, write):
351
352
 
352
353
  if not has_changes:
353
354
  click.secho(" ✅ Contract is already up to date.", fg="green")
355
+ if write:
356
+ mark_contract_fresh(str(cache_path))
354
357
  # Reported here too: a settings gap has nothing to do with whether
355
358
  # the contract changed, and an up to date contract is exactly when
356
359
  # nobody would think to look.
@@ -58,6 +58,10 @@ def _build_file_map(feature_type, short_name, docker=False):
58
58
  base_common = {
59
59
  ".gitignore": f"{T}feature_.gitignore.j2",
60
60
  "MANIFEST.in": f"{T}feature_MANIFEST.in.j2",
61
+ # pyproject.toml names it as the readme, and the package build reads
62
+ # it as the long description PyPI shows. Without the file the build
63
+ # warned and the published page was blank.
64
+ "README.md": f"{T}feature_README.md.j2",
61
65
  }
62
66
 
63
67
  if feature_type == "full":
@@ -9,6 +9,7 @@ from splent_cli.utils.feature_utils import (
9
9
  parse_feature_entry,
10
10
  read_feature_list,
11
11
  remove_feature_link,
12
+ set_feature_distribution,
12
13
  write_features_to_data,
13
14
  )
14
15
  from splent_cli.utils.io_utils import load_toml, atomic_write
@@ -108,6 +109,8 @@ def feature_detach(feature_identifier, version, force):
108
109
  changed = True
109
110
 
110
111
  if changed:
112
+ # Back to the editable checkout, which is never installed from PyPI.
113
+ set_feature_distribution(data, feature_name, None)
111
114
  atomic_write(pyproject_path, tomli_w.dumps(data))
112
115
 
113
116
  click.echo(f" {short}@{version} removed from pyproject.toml")
@@ -24,11 +24,12 @@ def _forge_label() -> str:
24
24
  )
25
25
  def feature_fork(feature_name, version):
26
26
  token = forge.token()
27
- github_user = os.getenv("GITHUB_USER")
27
+ github_user = forge.user()
28
28
 
29
29
  if not token or not github_user:
30
30
  click.secho(
31
- f"✖ Error: {forge.token_env_name()} and GITHUB_USER must be set", fg="red"
31
+ f"✖ Error: {forge.token_env_name()} and {forge.user_env_name()} must be set",
32
+ fg="red",
32
33
  )
33
34
  raise SystemExit(1)
34
35
 
@@ -28,6 +28,10 @@ Three things about this are worth stating plainly rather than discovering:
28
28
  * the summary says which features came from which channel. An image built
29
29
  from git tags is reproducible, but you should know that is what you have.
30
30
 
31
+ A feature published on PyPI under a name of its own (its [project].name is
32
+ not its directory name) is listed in [tool.splent.distributions], which
33
+ feature:attach writes, and is installed by that name.
34
+
31
35
  Example:
32
36
  splent-io/splent_feature_auth@v1.2.7 -> pip install splent_feature_auth==1.2.7
33
37
  -> or git+https://github.com/…@v1.2.7
@@ -41,6 +45,7 @@ import click
41
45
  import tomllib
42
46
 
43
47
  from splent_cli.services import context
48
+ from splent_cli.utils.feature_utils import feature_distribution
44
49
  from splent_cli.utils.git_url import https_url, namespace_spellings
45
50
  from splent_cli.utils.proc import run
46
51
 
@@ -100,7 +105,9 @@ def _pypi_does_not_have_it(output: str) -> bool:
100
105
  )
101
106
 
102
107
 
103
- def _git_candidates(namespace: str, name: str, ref: str) -> list[tuple[str, str]]:
108
+ def _git_candidates(
109
+ namespace: str, name: str, ref: str, dist: str | None = None
110
+ ) -> list[tuple[str, str]]:
104
111
  """Ordered (real, display) pip requirements for the tag, one per spelling.
105
112
 
106
113
  The entry carries the Python namespace (``splent_io``) while the repo
@@ -110,7 +117,9 @@ def _git_candidates(namespace: str, name: str, ref: str) -> list[tuple[str, str]
110
117
  candidates = []
111
118
  for spelling in namespace_spellings(namespace):
112
119
  real, display = https_url(spelling, name)
113
- candidates.append((f"{name} @ git+{real}@{ref}", f"git+{display}@{ref}"))
120
+ candidates.append(
121
+ (f"{dist or name} @ git+{real}@{ref}", f"git+{display}@{ref}")
122
+ )
114
123
  return candidates
115
124
 
116
125
 
@@ -190,7 +199,8 @@ def feature_pip_install(pypi_only):
190
199
  from_git = []
191
200
  for entry in features:
192
201
  namespace, name, version, ref = _parse_feature_entry(entry)
193
- spec = f"{name}=={version}" if version else name
202
+ dist = feature_distribution(data, name)
203
+ spec = f"{dist}=={version}" if version else dist
194
204
  if not version:
195
205
  unpinned.append(name)
196
206
 
@@ -216,7 +226,7 @@ def feature_pip_install(pypi_only):
216
226
  fg="yellow",
217
227
  )
218
228
  git_output = ""
219
- for real_spec, display_spec in _git_candidates(namespace, name, ref):
229
+ for real_spec, display_spec in _git_candidates(namespace, name, ref, dist):
220
230
  ok, git_output = _pip_install(real_spec)
221
231
  if ok:
222
232
  click.echo(f" ok {display_spec}")
@@ -14,7 +14,12 @@ import click
14
14
 
15
15
  from splent_cli.services import context
16
16
  from splent_cli.utils.feature_utils import normalize_namespace, read_features_from_data
17
- from splent_cli.utils.io_utils import atomic_write, backup_file, load_toml
17
+ from splent_cli.utils.io_utils import (
18
+ atomic_write,
19
+ backup_file,
20
+ give_tree_to_its_owner,
21
+ load_toml,
22
+ )
18
23
 
19
24
 
20
25
  DEFAULT_NAMESPACE = os.getenv("SPLENT_DEFAULT_NAMESPACE", "splent_io")
@@ -1040,6 +1045,10 @@ def feature_refinement(refiner_name):
1040
1045
  except Exception:
1041
1046
  pass
1042
1047
 
1048
+ # Everything above ran as root in the CLI container. templates/hooks/ was
1049
+ # left root's, and Tutorial 7 asks the reader to add files to it.
1050
+ give_tree_to_its_owner(refiner_path)
1051
+
1043
1052
  # ── Register the refinement in the active product ───────────────
1044
1053
  # A refinement only takes effect once the feature is part of the product
1045
1054
  # (migrations, composition, feature:xray). Add it now so the user does not