splent-cli 1.23.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.23.0/src/splent_cli.egg-info → splent_cli-1.25.0}/PKG-INFO +4 -1
  2. {splent_cli-1.23.0 → splent_cli-1.25.0}/pyproject.toml +10 -1
  3. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/cli.py +14 -1
  4. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/commands/check/check_env.py +7 -4
  5. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/commands/check/check_features.py +11 -3
  6. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/commands/check/check_github.py +28 -14
  7. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/commands/check/check_infra.py +21 -5
  8. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/commands/check/check_releases.py +16 -12
  9. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/commands/database/db_migrate.py +52 -14
  10. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/commands/database/db_reset.py +1 -0
  11. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/commands/database/db_rollback.py +1 -0
  12. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/commands/database/db_seed.py +1 -0
  13. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/commands/database/db_status.py +4 -2
  14. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/commands/doctor.py +4 -4
  15. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/commands/env/env_set.py +20 -9
  16. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/commands/export/export_puml.py +1 -1
  17. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/commands/feature/feature_attach.py +14 -0
  18. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/commands/feature/feature_contract.py +14 -0
  19. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/commands/feature/feature_create.py +4 -0
  20. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/commands/feature/feature_detach.py +3 -0
  21. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/commands/feature/feature_fork.py +3 -2
  22. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/commands/feature/feature_impact.py +1 -1
  23. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/commands/feature/feature_pip_install.py +14 -4
  24. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/commands/feature/feature_refine.py +125 -24
  25. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/commands/feature/feature_release.py +298 -25
  26. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/commands/feature/feature_remove.py +4 -3
  27. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/commands/feature/feature_rename.py +5 -2
  28. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/commands/feature/feature_test.py +104 -1
  29. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/commands/marketplace/marketplace_index.py +1 -1
  30. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/commands/product/product_build.py +16 -8
  31. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/commands/product/product_configure.py +1 -1
  32. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/commands/product/product_containers.py +4 -1
  33. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/commands/product/product_create.py +5 -4
  34. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/commands/product/product_deploy.py +32 -27
  35. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/commands/product/product_derive.py +31 -7
  36. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/commands/product/product_down.py +12 -4
  37. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/commands/product/product_drift.py +8 -0
  38. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/commands/product/product_env.py +31 -5
  39. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/commands/product/product_release.py +22 -9
  40. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/commands/product/product_run.py +59 -0
  41. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/commands/product/product_shell.py +21 -1
  42. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/commands/product/product_sync_template.py +9 -1
  43. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/commands/release/release_core.py +23 -12
  44. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/commands/release/release_resume.py +39 -11
  45. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/commands/spl/spl_add_constraints.py +2 -1
  46. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/commands/spl/spl_add_feature.py +21 -7
  47. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/commands/spl/spl_remove_feature.py +2 -1
  48. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/commands/spl/spl_utils.py +34 -0
  49. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/commands/tokens_setup.py +14 -5
  50. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/services/compose.py +12 -2
  51. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/services/context.py +35 -0
  52. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/services/preflight.py +6 -2
  53. splent_cli-1.25.0/src/splent_cli/services/pypi_quota.py +538 -0
  54. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/services/registry.py +16 -10
  55. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/services/release.py +281 -70
  56. splent_cli-1.25.0/src/splent_cli/services/release_dry_run.py +563 -0
  57. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/services/release_gate.py +84 -49
  58. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/services/spl_store.py +86 -3
  59. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/utils/contract_freshness.py +27 -4
  60. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/utils/dynamic_imports.py +50 -5
  61. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/utils/feature_utils.py +63 -0
  62. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/utils/forge.py +25 -4
  63. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/utils/io_utils.py +86 -1
  64. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/utils/template_drift.py +1 -0
  65. {splent_cli-1.23.0 → splent_cli-1.25.0/src/splent_cli.egg-info}/PKG-INFO +4 -1
  66. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli.egg-info/SOURCES.txt +2 -0
  67. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli.egg-info/requires.txt +3 -0
  68. {splent_cli-1.23.0 → splent_cli-1.25.0}/LICENSE +0 -0
  69. {splent_cli-1.23.0 → splent_cli-1.25.0}/README.md +0 -0
  70. {splent_cli-1.23.0 → splent_cli-1.25.0}/setup.cfg +0 -0
  71. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/__init__.py +0 -0
  72. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/__main__.py +0 -0
  73. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/commands/__init__.py +0 -0
  74. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/commands/cache/__init__.py +0 -0
  75. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/commands/cache/cache_clear.py +0 -0
  76. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/commands/cache/cache_orphans.py +0 -0
  77. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/commands/cache/cache_outdated.py +0 -0
  78. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/commands/cache/cache_prune.py +0 -0
  79. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/commands/cache/cache_size.py +0 -0
  80. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/commands/cache/cache_status.py +0 -0
  81. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/commands/cache/cache_usage.py +0 -0
  82. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/commands/cache/cache_versions.py +0 -0
  83. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/commands/check/__init__.py +0 -0
  84. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/commands/check/check_contracts.py +0 -0
  85. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/commands/check/check_deps.py +0 -0
  86. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/commands/check/check_docker.py +0 -0
  87. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/commands/check/check_product.py +0 -0
  88. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/commands/check/check_pypi.py +0 -0
  89. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/commands/check/check_pyproject.py +0 -0
  90. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/commands/clear/__init__.py +0 -0
  91. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/commands/clear/clear_build.py +0 -0
  92. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/commands/clear/clear_log.py +0 -0
  93. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/commands/clear/clear_uploads.py +0 -0
  94. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/commands/command/__init__.py +0 -0
  95. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/commands/command/command_create.py +0 -0
  96. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/commands/coverage.py +0 -0
  97. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/commands/database/db_console.py +0 -0
  98. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/commands/database/db_dump.py +0 -0
  99. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/commands/database/db_restore.py +0 -0
  100. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/commands/database/db_upgrade.py +0 -0
  101. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/commands/env/env_list.py +0 -0
  102. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/commands/env/env_show.py +0 -0
  103. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/commands/export/__init__.py +0 -0
  104. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/commands/feature/feature_add.py +0 -0
  105. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/commands/feature/feature_clean.py +0 -0
  106. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/commands/feature/feature_clone.py +0 -0
  107. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/commands/feature/feature_compat.py +0 -0
  108. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/commands/feature/feature_compile.py +0 -0
  109. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/commands/feature/feature_delete.py +0 -0
  110. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/commands/feature/feature_discard.py +0 -0
  111. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/commands/feature/feature_drift.py +0 -0
  112. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/commands/feature/feature_env.py +0 -0
  113. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/commands/feature/feature_git.py +0 -0
  114. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/commands/feature/feature_hook_add.py +0 -0
  115. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/commands/feature/feature_hook_remove.py +0 -0
  116. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/commands/feature/feature_hooks.py +0 -0
  117. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/commands/feature/feature_info.py +0 -0
  118. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/commands/feature/feature_inject_config.py +0 -0
  119. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/commands/feature/feature_install.py +0 -0
  120. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/commands/feature/feature_list.py +0 -0
  121. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/commands/feature/feature_order.py +0 -0
  122. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/commands/feature/feature_outdated.py +0 -0
  123. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/commands/feature/feature_pin.py +0 -0
  124. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/commands/feature/feature_pull.py +0 -0
  125. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/commands/feature/feature_search.py +0 -0
  126. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/commands/feature/feature_status.py +0 -0
  127. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/commands/feature/feature_sync_template.py +0 -0
  128. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/commands/feature/feature_translate.py +0 -0
  129. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/commands/feature/feature_unlock.py +0 -0
  130. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/commands/feature/feature_upgrade.py +0 -0
  131. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/commands/feature/feature_versions.py +0 -0
  132. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/commands/feature/feature_xray.py +0 -0
  133. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/commands/lint.py +0 -0
  134. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/commands/locust.py +0 -0
  135. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/commands/marketplace/login.py +0 -0
  136. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/commands/marketplace/logout.py +0 -0
  137. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/commands/marketplace/marketplace_auth_utils.py +0 -0
  138. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/commands/marketplace/whoami.py +0 -0
  139. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/commands/product/__init__.py +0 -0
  140. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/commands/product/product_auto_require.py +0 -0
  141. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/commands/product/product_clean.py +0 -0
  142. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/commands/product/product_commands.py +0 -0
  143. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/commands/product/product_config.py +0 -0
  144. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/commands/product/product_console.py +0 -0
  145. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/commands/product/product_deselect.py +0 -0
  146. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/commands/product/product_list.py +0 -0
  147. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/commands/product/product_logs.py +0 -0
  148. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/commands/product/product_missing.py +0 -0
  149. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/commands/product/product_port.py +0 -0
  150. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/commands/product/product_resolve.py +0 -0
  151. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/commands/product/product_restart.py +0 -0
  152. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/commands/product/product_routes.py +0 -0
  153. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/commands/product/product_select.py +0 -0
  154. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/commands/product/product_signals.py +0 -0
  155. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/commands/product/product_test.py +0 -0
  156. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/commands/product/product_up.py +0 -0
  157. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/commands/product/product_validate.py +0 -0
  158. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/commands/release/__init__.py +0 -0
  159. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/commands/selenium.py +0 -0
  160. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/commands/spl/__init__.py +0 -0
  161. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/commands/spl/spl_compare.py +0 -0
  162. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/commands/spl/spl_configurations.py +0 -0
  163. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/commands/spl/spl_create.py +0 -0
  164. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/commands/spl/spl_deps.py +0 -0
  165. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/commands/spl/spl_features.py +0 -0
  166. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/commands/spl/spl_fetch.py +0 -0
  167. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/commands/spl/spl_info.py +0 -0
  168. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/commands/spl/spl_list.py +0 -0
  169. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/commands/spl/spl_migrate_catalog.py +0 -0
  170. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/commands/spl/spl_outdated.py +0 -0
  171. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/commands/spl/spl_pin.py +0 -0
  172. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/commands/spl/spl_publish.py +0 -0
  173. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/commands/uvl/uvl_utils.py +0 -0
  174. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/commands/version.py +0 -0
  175. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/services/__init__.py +0 -0
  176. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/services/credentials.py +0 -0
  177. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/services/index_refresh.py +0 -0
  178. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/services/marketplace.py +0 -0
  179. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/services/marketplace_api.py +0 -0
  180. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/services/marketplace_url.py +0 -0
  181. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/utils/__init__.py +0 -0
  182. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/utils/archetype.py +0 -0
  183. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/utils/cache_utils.py +0 -0
  184. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/utils/command_loader.py +0 -0
  185. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/utils/db_utils.py +0 -0
  186. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/utils/decorators.py +0 -0
  187. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/utils/feature_installer.py +0 -0
  188. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/utils/git_url.py +0 -0
  189. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/utils/integrity.py +0 -0
  190. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/utils/lifecycle.py +0 -0
  191. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/utils/manifest.py +0 -0
  192. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/utils/path_utils.py +0 -0
  193. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli/utils/proc.py +0 -0
  194. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli.egg-info/dependency_links.txt +0 -0
  195. {splent_cli-1.23.0 → splent_cli-1.25.0}/src/splent_cli.egg-info/entry_points.txt +0 -0
  196. {splent_cli-1.23.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.23.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
@@ -9,9 +9,12 @@ Requires-Python: >=3.13
9
9
  Description-Content-Type: text/markdown
10
10
  License-File: LICENSE
11
11
  Requires-Dist: click==8.1.8
12
+ Requires-Dist: jinja2>=3.1
12
13
  Requires-Dist: packaging>=24.0
14
+ Requires-Dist: python-dotenv>=1.0
13
15
  Requires-Dist: pyyaml>=6.0
14
16
  Requires-Dist: requests>=2.31.0
17
+ Requires-Dist: splent_framework<2,>=1.15.1
15
18
  Requires-Dist: tomli_w>=1.0.0
16
19
  Provides-Extra: dev
17
20
  Requires-Dist: setuptools==80.3.1; extra == "dev"
@@ -4,18 +4,27 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "splent_cli"
7
- version = "1.23.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"
11
11
  authors = [{ name = "DiversoLab", email = "diversolab@us.es" }]
12
12
  license-files = ["LICENSE"]
13
13
 
14
+ # Everything the CLI imports at module level must be listed here (or come
15
+ # with splent_framework, see tests/unit/test_declared_dependencies.py):
16
+ # startup imports every command module, so one undeclared import breaks
17
+ # `splent --help` on a clean `pip install splent_cli`.
14
18
  dependencies = [
15
19
  "click==8.1.8",
20
+ "jinja2>=3.1",
16
21
  "packaging>=24.0",
22
+ "python-dotenv>=1.0",
17
23
  "pyyaml>=6.0",
18
24
  "requests>=2.31.0",
25
+ # The CLI drives the framework (paths, feature order, migrations, the
26
+ # app factory) and only works with the same major version of it.
27
+ "splent_framework>=1.15.1,<2",
19
28
  "tomli_w>=1.0.0",
20
29
  ]
21
30
 
@@ -1,3 +1,4 @@
1
+ import logging
1
2
  import os
2
3
 
3
4
  import click
@@ -37,6 +38,15 @@ class SPLENTCLI(click.Group):
37
38
  return self._feature_cmds_cache
38
39
 
39
40
  self._feature_cmds_cache: dict[str, click.BaseCommand] = {}
41
+ # Discovery runs before every command, including the ones a product
42
+ # that is not built yet (selected, not configured or derived) runs
43
+ # while getting there. Building its app then fails, and the framework
44
+ # logged that failure with a full traceback on every single command,
45
+ # although nothing was wrong. The failure is expected here and
46
+ # handled below, so the framework's logging is muted for the attempt.
47
+ debug = bool(os.getenv("SPLENT_DEBUG"))
48
+ if not debug:
49
+ logging.disable(logging.CRITICAL)
40
50
  try:
41
51
  from splent_cli.utils.dynamic_imports import get_app
42
52
 
@@ -57,10 +67,13 @@ class SPLENTCLI(click.Group):
57
67
  group.add_command(cmd)
58
68
  self._feature_cmds_cache[group.name] = group
59
69
  except Exception as e:
60
- if os.getenv("SPLENT_DEBUG"):
70
+ if debug:
61
71
  click.secho(
62
72
  f" ⚠ Feature commands not loaded: {e}", fg="yellow", err=True
63
73
  )
74
+ finally:
75
+ if not debug:
76
+ logging.disable(logging.NOTSET)
64
77
  return self._feature_cmds_cache
65
78
 
66
79
  def get_command(self, ctx, cmd_name):
@@ -8,6 +8,8 @@ import sys
8
8
  import click
9
9
  import importlib.metadata
10
10
 
11
+ from splent_cli.utils import forge
12
+
11
13
 
12
14
  def _pkg_version(name: str) -> str | None:
13
15
  try:
@@ -88,11 +90,12 @@ def check_env():
88
90
  f"CLI={'?' if not cli_v else cli_v} / Framework={'?' if not fw_v else fw_v}"
89
91
  )
90
92
 
91
- # Credentials
92
- if os.getenv("GITHUB_TOKEN"):
93
- _ok("GITHUB_TOKEN set")
93
+ # Credentials. The forge token is named after the forge in use: asking for
94
+ # GITHUB_TOKEN on a git.splent.io workspace sent people to the wrong place.
95
+ if forge.token():
96
+ _ok(f"{forge.token_env_name()} set ({forge.host()})")
94
97
  else:
95
- _warn("GITHUB_TOKEN not set")
98
+ _warn(f"{forge.token_env_name()} not set ({forge.host()})")
96
99
 
97
100
  if os.getenv("TWINE_PASSWORD") or os.getenv("PYPI_TOKEN"):
98
101
  _ok("PyPI token set")
@@ -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]
@@ -382,17 +382,33 @@ def check_infra():
382
382
  else:
383
383
  _ok(f"No port conflicts ({len(all_ports)} ports declared)")
384
384
 
385
- # Check against running containers (query docker ps once, not per-port)
385
+ # Check against running containers (query docker ps once, not per-port).
386
+ # The product's own stacks are not conflicts: with the product up, every
387
+ # declared port is held by exactly the container meant to hold it, and
388
+ # reporting those as warnings made a healthy product look broken.
389
+ own_projects = {compose.project_name(product, env)} | {
390
+ compose.feature_project_name(compose.normalize_feature_ref(f), product, env)
391
+ for f in features
392
+ }
386
393
  running_conflicts = []
387
- ps_result = _run(["docker", "ps", "--format", "{{.ID}}\t{{.Names}}\t{{.Ports}}"])
394
+ ps_result = _run(
395
+ [
396
+ "docker",
397
+ "ps",
398
+ "--format",
399
+ '{{.ID}}\t{{.Names}}\t{{.Label "com.docker.compose.project"}}\t{{.Ports}}',
400
+ ]
401
+ )
388
402
  if ps_result.returncode == 0:
389
403
  ps_lines = ps_result.stdout.splitlines()
390
404
  for port in all_ports:
391
405
  for line in ps_lines:
392
- parts = line.split("\t", 2)
393
- if len(parts) < 3:
406
+ parts = line.split("\t", 3)
407
+ if len(parts) < 4:
408
+ continue
409
+ cid, name, project, ports_str = parts
410
+ if project in own_projects:
394
411
  continue
395
- cid, name, ports_str = parts
396
412
  if f":{port}->" in ports_str:
397
413
  running_conflicts.append((port, name))
398
414
 
@@ -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
 
@@ -1,4 +1,6 @@
1
1
  import os
2
+ import shutil
3
+ import subprocess
2
4
 
3
5
  import click
4
6
  from flask_migrate import migrate as alembic_migrate
@@ -46,10 +48,39 @@ def _is_empty_migration(path: str) -> bool:
46
48
  return True
47
49
 
48
50
 
51
+ def _format_migration(path: str) -> None:
52
+ """Run ``ruff format`` on a freshly generated migration, if ruff exists.
53
+
54
+ Alembic writes single quotes and one long create_table() call, which the
55
+ release gate's ``ruff format --check`` then rejects: the first release of
56
+ any feature with a model failed lint on a file nobody wrote by hand.
57
+ """
58
+ ruff = shutil.which("ruff")
59
+ if not ruff:
60
+ return
61
+ subprocess.run([ruff, "format", "--quiet", path], check=False)
62
+
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
+
49
80
  @requires_db
50
81
  @click.command(
51
82
  "db:migrate",
52
- short_help="Generate a new migration from model changes and apply it.",
83
+ short_help="Generate a new migration from model changes (db:upgrade applies it).",
53
84
  )
54
85
  @click.argument("feature", required=False, default=None)
55
86
  @click.option(
@@ -67,8 +98,8 @@ def _is_empty_migration(path: str) -> bool:
67
98
  @context.requires_product
68
99
  def db_migrate(feature, message, empty):
69
100
  """
70
- Generate new migration files for features that have schema changes,
71
- then apply all pending migrations.
101
+ Generate new migration files for features that have schema changes.
102
+ Nothing is applied: run ``splent db:upgrade`` for that.
72
103
 
73
104
  Empty migrations (no schema changes detected) are automatically removed,
74
105
  unless ``--empty`` is passed, in which case a blank migration is created
@@ -158,10 +189,24 @@ def db_migrate(feature, message, empty):
158
189
 
159
190
  after = _count_versions(mdir)
160
191
 
192
+ newest = None
193
+ if after > before:
194
+ versions_dir = os.path.join(mdir, "versions")
195
+ newest = max(
196
+ (
197
+ os.path.join(versions_dir, f)
198
+ for f in os.listdir(versions_dir)
199
+ if f.endswith(".py")
200
+ ),
201
+ key=os.path.getmtime,
202
+ )
203
+
161
204
  # A blank migration requested with --empty is intentional: keep it so the
162
205
  # user can fill in upgrade()/downgrade() by hand (e.g. refinement columns).
163
206
  if empty:
164
- if after > before:
207
+ if newest:
208
+ _format_migration(newest)
209
+ _hand_to_directory_owner(newest)
165
210
  click.echo(
166
211
  click.style(
167
212
  f" 📝 {feat}: blank migration generated — "
@@ -174,20 +219,13 @@ def db_migrate(feature, message, empty):
174
219
  continue
175
220
 
176
221
  # If a new file was generated, check if it's empty (no real schema changes)
177
- if after > before:
178
- versions_dir = os.path.join(mdir, "versions")
179
- newest = max(
180
- (
181
- os.path.join(versions_dir, f)
182
- for f in os.listdir(versions_dir)
183
- if f.endswith(".py")
184
- ),
185
- key=os.path.getmtime,
186
- )
222
+ if newest:
187
223
  if _is_empty_migration(newest):
188
224
  os.remove(newest)
189
225
  click.echo(click.style(f" ✔ {feat}: up to date", fg="green"))
190
226
  else:
227
+ _format_migration(newest)
228
+ _hand_to_directory_owner(newest)
191
229
  click.echo(
192
230
  click.style(f" 📝 {feat}: new migration generated", fg="cyan")
193
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(
@@ -104,8 +104,10 @@ def db_status():
104
104
  mdir = dirs.get(feat)
105
105
  fs_head = _get_filesystem_head(mdir) if mdir else None
106
106
 
107
- db_display = db_rev[:12] if db_rev else "—"
108
- fs_display = fs_head[:12] if fs_head else "—"
107
+ # Cut to the column, not to 12: a hex revision is 12 characters but a
108
+ # named one is not, and "auth0002_role" showed as "auth0002_rol".
109
+ db_display = db_rev[:col_rev] if db_rev else "—"
110
+ fs_display = fs_head[:col_rev] if fs_head else "—"
109
111
 
110
112
  if db_rev and fs_head and db_rev == fs_head:
111
113
  status = click.style("✔ synced", fg="green")
@@ -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("--------------------------------")
@@ -817,7 +817,7 @@ def export_puml(
817
817
  if not uvl_path:
818
818
  click.secho(
819
819
  "❌ No UVL file found.\n"
820
- " Set [tool.splent].spl in pyproject.toml (SPL catalog)\n"
820
+ " Set [tool.splent].spl in pyproject.toml (the SPL model)\n"
821
821
  " or [tool.splent.uvl].file (legacy).",
822
822
  fg="red",
823
823
  )
@@ -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)