splent-cli 1.26.1__tar.gz → 1.27.1__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 (217) hide show
  1. {splent_cli-1.26.1/src/splent_cli.egg-info → splent_cli-1.27.1}/PKG-INFO +2 -2
  2. {splent_cli-1.26.1 → splent_cli-1.27.1}/pyproject.toml +2 -2
  3. splent_cli-1.27.1/src/splent_cli/__init__.py +15 -0
  4. {splent_cli-1.26.1 → splent_cli-1.27.1}/src/splent_cli/cli.py +2 -0
  5. splent_cli-1.27.1/src/splent_cli/commands/cache/cache_clear.py +208 -0
  6. splent_cli-1.27.1/src/splent_cli/commands/cache/cache_orphans.py +58 -0
  7. {splent_cli-1.26.1 → splent_cli-1.27.1}/src/splent_cli/commands/cache/cache_outdated.py +35 -41
  8. splent_cli-1.27.1/src/splent_cli/commands/cache/cache_prune.py +148 -0
  9. {splent_cli-1.26.1 → splent_cli-1.27.1}/src/splent_cli/commands/cache/cache_status.py +5 -19
  10. {splent_cli-1.26.1 → splent_cli-1.27.1}/src/splent_cli/commands/cache/cache_usage.py +14 -21
  11. splent_cli-1.27.1/src/splent_cli/commands/cache/cache_versions.py +100 -0
  12. {splent_cli-1.26.1 → splent_cli-1.27.1}/src/splent_cli/commands/check/check_contracts.py +3 -5
  13. {splent_cli-1.26.1 → splent_cli-1.27.1}/src/splent_cli/commands/check/check_deps.py +15 -2
  14. {splent_cli-1.26.1 → splent_cli-1.27.1}/src/splent_cli/commands/check/check_env.py +7 -3
  15. {splent_cli-1.26.1 → splent_cli-1.27.1}/src/splent_cli/commands/check/check_features.py +2 -2
  16. {splent_cli-1.26.1 → splent_cli-1.27.1}/src/splent_cli/commands/check/check_github.py +28 -5
  17. {splent_cli-1.26.1 → splent_cli-1.27.1}/src/splent_cli/commands/check/check_infra.py +134 -60
  18. {splent_cli-1.26.1 → splent_cli-1.27.1}/src/splent_cli/commands/check/check_product.py +32 -12
  19. {splent_cli-1.26.1 → splent_cli-1.27.1}/src/splent_cli/commands/check/check_pypi.py +6 -6
  20. {splent_cli-1.26.1 → splent_cli-1.27.1}/src/splent_cli/commands/check/check_pyproject.py +47 -17
  21. {splent_cli-1.26.1 → splent_cli-1.27.1}/src/splent_cli/commands/check/check_releases.py +12 -0
  22. {splent_cli-1.26.1 → splent_cli-1.27.1}/src/splent_cli/commands/clear/clear_log.py +1 -0
  23. splent_cli-1.27.1/src/splent_cli/commands/clear/clear_uploads.py +110 -0
  24. splent_cli-1.27.1/src/splent_cli/commands/command/command_create.py +97 -0
  25. {splent_cli-1.26.1 → splent_cli-1.27.1}/src/splent_cli/commands/coverage.py +11 -2
  26. {splent_cli-1.26.1 → splent_cli-1.27.1}/src/splent_cli/commands/database/db_console.py +3 -1
  27. {splent_cli-1.26.1 → splent_cli-1.27.1}/src/splent_cli/commands/database/db_dump.py +38 -9
  28. {splent_cli-1.26.1 → splent_cli-1.27.1}/src/splent_cli/commands/database/db_migrate.py +71 -26
  29. {splent_cli-1.26.1 → splent_cli-1.27.1}/src/splent_cli/commands/database/db_reset.py +169 -43
  30. {splent_cli-1.26.1 → splent_cli-1.27.1}/src/splent_cli/commands/database/db_restore.py +40 -10
  31. {splent_cli-1.26.1 → splent_cli-1.27.1}/src/splent_cli/commands/database/db_rollback.py +81 -18
  32. {splent_cli-1.26.1 → splent_cli-1.27.1}/src/splent_cli/commands/database/db_seed.py +148 -44
  33. {splent_cli-1.26.1 → splent_cli-1.27.1}/src/splent_cli/commands/database/db_status.py +36 -20
  34. {splent_cli-1.26.1 → splent_cli-1.27.1}/src/splent_cli/commands/database/db_upgrade.py +111 -12
  35. {splent_cli-1.26.1 → splent_cli-1.27.1}/src/splent_cli/commands/doctor.py +12 -1
  36. {splent_cli-1.26.1 → splent_cli-1.27.1}/src/splent_cli/commands/env/env_list.py +16 -26
  37. splent_cli-1.27.1/src/splent_cli/commands/env/env_set.py +265 -0
  38. splent_cli-1.27.1/src/splent_cli/commands/env/env_show.py +52 -0
  39. {splent_cli-1.26.1 → splent_cli-1.27.1}/src/splent_cli/commands/export/export_puml.py +264 -122
  40. {splent_cli-1.26.1 → splent_cli-1.27.1}/src/splent_cli/commands/feature/feature_add.py +21 -10
  41. {splent_cli-1.26.1 → splent_cli-1.27.1}/src/splent_cli/commands/feature/feature_attach.py +53 -14
  42. splent_cli-1.27.1/src/splent_cli/commands/feature/feature_clean.py +583 -0
  43. {splent_cli-1.26.1 → splent_cli-1.27.1}/src/splent_cli/commands/feature/feature_clone.py +26 -14
  44. {splent_cli-1.26.1 → splent_cli-1.27.1}/src/splent_cli/commands/feature/feature_compat.py +37 -4
  45. {splent_cli-1.26.1 → splent_cli-1.27.1}/src/splent_cli/commands/feature/feature_compile.py +23 -5
  46. {splent_cli-1.26.1 → splent_cli-1.27.1}/src/splent_cli/commands/feature/feature_contract.py +46 -5
  47. {splent_cli-1.26.1 → splent_cli-1.27.1}/src/splent_cli/commands/feature/feature_create.py +8 -1
  48. {splent_cli-1.26.1 → splent_cli-1.27.1}/src/splent_cli/commands/feature/feature_delete.py +24 -7
  49. {splent_cli-1.26.1 → splent_cli-1.27.1}/src/splent_cli/commands/feature/feature_detach.py +29 -4
  50. splent_cli-1.27.1/src/splent_cli/commands/feature/feature_drift.py +285 -0
  51. splent_cli-1.27.1/src/splent_cli/commands/feature/feature_hook_add.py +406 -0
  52. {splent_cli-1.26.1 → splent_cli-1.27.1}/src/splent_cli/commands/feature/feature_hook_remove.py +66 -81
  53. {splent_cli-1.26.1 → splent_cli-1.27.1}/src/splent_cli/commands/feature/feature_info.py +3 -2
  54. splent_cli-1.27.1/src/splent_cli/commands/feature/feature_inject_config.py +374 -0
  55. {splent_cli-1.26.1 → splent_cli-1.27.1}/src/splent_cli/commands/feature/feature_install.py +135 -75
  56. {splent_cli-1.26.1 → splent_cli-1.27.1}/src/splent_cli/commands/feature/feature_list.py +7 -4
  57. {splent_cli-1.26.1 → splent_cli-1.27.1}/src/splent_cli/commands/feature/feature_order.py +27 -6
  58. {splent_cli-1.26.1 → splent_cli-1.27.1}/src/splent_cli/commands/feature/feature_outdated.py +15 -11
  59. {splent_cli-1.26.1 → splent_cli-1.27.1}/src/splent_cli/commands/feature/feature_pin.py +24 -7
  60. {splent_cli-1.26.1 → splent_cli-1.27.1}/src/splent_cli/commands/feature/feature_pip_install.py +14 -4
  61. {splent_cli-1.26.1 → splent_cli-1.27.1}/src/splent_cli/commands/feature/feature_refine.py +128 -59
  62. {splent_cli-1.26.1 → splent_cli-1.27.1}/src/splent_cli/commands/feature/feature_release.py +299 -105
  63. {splent_cli-1.26.1 → splent_cli-1.27.1}/src/splent_cli/commands/feature/feature_remove.py +22 -3
  64. {splent_cli-1.26.1 → splent_cli-1.27.1}/src/splent_cli/commands/feature/feature_rename.py +87 -41
  65. {splent_cli-1.26.1 → splent_cli-1.27.1}/src/splent_cli/commands/feature/feature_search.py +11 -8
  66. {splent_cli-1.26.1 → splent_cli-1.27.1}/src/splent_cli/commands/feature/feature_status.py +46 -13
  67. {splent_cli-1.26.1 → splent_cli-1.27.1}/src/splent_cli/commands/feature/feature_sync_template.py +52 -4
  68. {splent_cli-1.26.1 → splent_cli-1.27.1}/src/splent_cli/commands/feature/feature_test.py +135 -26
  69. {splent_cli-1.26.1 → splent_cli-1.27.1}/src/splent_cli/commands/feature/feature_translate.py +40 -11
  70. {splent_cli-1.26.1 → splent_cli-1.27.1}/src/splent_cli/commands/feature/feature_unlock.py +73 -2
  71. {splent_cli-1.26.1 → splent_cli-1.27.1}/src/splent_cli/commands/feature/feature_upgrade.py +62 -18
  72. {splent_cli-1.26.1 → splent_cli-1.27.1}/src/splent_cli/commands/feature/feature_versions.py +89 -32
  73. {splent_cli-1.26.1 → splent_cli-1.27.1}/src/splent_cli/commands/feature/feature_xray.py +19 -22
  74. {splent_cli-1.26.1 → splent_cli-1.27.1}/src/splent_cli/commands/lint.py +5 -1
  75. splent_cli-1.27.1/src/splent_cli/commands/locust.py +305 -0
  76. {splent_cli-1.26.1 → splent_cli-1.27.1}/src/splent_cli/commands/marketplace/login.py +22 -0
  77. {splent_cli-1.26.1 → splent_cli-1.27.1}/src/splent_cli/commands/marketplace/logout.py +42 -2
  78. {splent_cli-1.26.1 → splent_cli-1.27.1}/src/splent_cli/commands/marketplace/marketplace_index.py +24 -10
  79. {splent_cli-1.26.1 → splent_cli-1.27.1}/src/splent_cli/commands/product/product_auto_require.py +55 -9
  80. {splent_cli-1.26.1 → splent_cli-1.27.1}/src/splent_cli/commands/product/product_build.py +296 -90
  81. {splent_cli-1.26.1 → splent_cli-1.27.1}/src/splent_cli/commands/product/product_clean.py +45 -36
  82. {splent_cli-1.26.1 → splent_cli-1.27.1}/src/splent_cli/commands/product/product_config.py +53 -7
  83. {splent_cli-1.26.1 → splent_cli-1.27.1}/src/splent_cli/commands/product/product_containers.py +34 -17
  84. {splent_cli-1.26.1 → splent_cli-1.27.1}/src/splent_cli/commands/product/product_create.py +80 -17
  85. {splent_cli-1.26.1 → splent_cli-1.27.1}/src/splent_cli/commands/product/product_deploy.py +106 -50
  86. {splent_cli-1.26.1 → splent_cli-1.27.1}/src/splent_cli/commands/product/product_derive.py +44 -21
  87. splent_cli-1.27.1/src/splent_cli/commands/product/product_down.py +211 -0
  88. {splent_cli-1.26.1 → splent_cli-1.27.1}/src/splent_cli/commands/product/product_drift.py +4 -4
  89. {splent_cli-1.26.1 → splent_cli-1.27.1}/src/splent_cli/commands/product/product_env.py +93 -130
  90. {splent_cli-1.26.1 → splent_cli-1.27.1}/src/splent_cli/commands/product/product_missing.py +6 -4
  91. {splent_cli-1.26.1 → splent_cli-1.27.1}/src/splent_cli/commands/product/product_port.py +37 -24
  92. {splent_cli-1.26.1 → splent_cli-1.27.1}/src/splent_cli/commands/product/product_release.py +75 -20
  93. {splent_cli-1.26.1 → splent_cli-1.27.1}/src/splent_cli/commands/product/product_resolve.py +42 -4
  94. {splent_cli-1.26.1 → splent_cli-1.27.1}/src/splent_cli/commands/product/product_restart.py +72 -21
  95. {splent_cli-1.26.1 → splent_cli-1.27.1}/src/splent_cli/commands/product/product_run.py +28 -24
  96. {splent_cli-1.26.1 → splent_cli-1.27.1}/src/splent_cli/commands/product/product_select.py +35 -0
  97. {splent_cli-1.26.1 → splent_cli-1.27.1}/src/splent_cli/commands/product/product_shell.py +35 -15
  98. {splent_cli-1.26.1 → splent_cli-1.27.1}/src/splent_cli/commands/product/product_sync_template.py +7 -6
  99. {splent_cli-1.26.1 → splent_cli-1.27.1}/src/splent_cli/commands/product/product_test.py +7 -3
  100. {splent_cli-1.26.1 → splent_cli-1.27.1}/src/splent_cli/commands/product/product_up.py +65 -19
  101. {splent_cli-1.26.1 → splent_cli-1.27.1}/src/splent_cli/commands/release/release_core.py +4 -0
  102. splent_cli-1.27.1/src/splent_cli/commands/release/release_resume.py +760 -0
  103. {splent_cli-1.26.1 → splent_cli-1.27.1}/src/splent_cli/commands/selenium.py +34 -12
  104. {splent_cli-1.26.1 → splent_cli-1.27.1}/src/splent_cli/commands/spl/spl_add_constraints.py +19 -5
  105. {splent_cli-1.26.1 → splent_cli-1.27.1}/src/splent_cli/commands/spl/spl_add_feature.py +40 -2
  106. {splent_cli-1.26.1 → splent_cli-1.27.1}/src/splent_cli/commands/spl/spl_compare.py +6 -3
  107. {splent_cli-1.26.1 → splent_cli-1.27.1}/src/splent_cli/commands/spl/spl_create.py +43 -0
  108. {splent_cli-1.26.1 → splent_cli-1.27.1}/src/splent_cli/commands/spl/spl_deps.py +21 -0
  109. {splent_cli-1.26.1 → splent_cli-1.27.1}/src/splent_cli/commands/spl/spl_outdated.py +64 -19
  110. splent_cli-1.27.1/src/splent_cli/commands/spl/spl_pin.py +158 -0
  111. {splent_cli-1.26.1 → splent_cli-1.27.1}/src/splent_cli/commands/spl/spl_publish.py +73 -12
  112. splent_cli-1.27.1/src/splent_cli/commands/spl/spl_remove_feature.py +254 -0
  113. {splent_cli-1.26.1 → splent_cli-1.27.1}/src/splent_cli/commands/uvl/uvl_utils.py +4 -7
  114. {splent_cli-1.26.1 → splent_cli-1.27.1}/src/splent_cli/commands/version.py +33 -8
  115. {splent_cli-1.26.1 → splent_cli-1.27.1}/src/splent_cli/services/compose.py +127 -40
  116. {splent_cli-1.26.1 → splent_cli-1.27.1}/src/splent_cli/services/context.py +63 -4
  117. {splent_cli-1.26.1 → splent_cli-1.27.1}/src/splent_cli/services/credentials.py +10 -1
  118. {splent_cli-1.26.1 → splent_cli-1.27.1}/src/splent_cli/services/marketplace.py +69 -8
  119. {splent_cli-1.26.1 → splent_cli-1.27.1}/src/splent_cli/services/marketplace_api.py +26 -1
  120. {splent_cli-1.26.1 → splent_cli-1.27.1}/src/splent_cli/services/marketplace_url.py +31 -4
  121. {splent_cli-1.26.1 → splent_cli-1.27.1}/src/splent_cli/services/preflight.py +55 -10
  122. {splent_cli-1.26.1 → splent_cli-1.27.1}/src/splent_cli/services/registry.py +17 -2
  123. {splent_cli-1.26.1 → splent_cli-1.27.1}/src/splent_cli/services/release.py +274 -55
  124. {splent_cli-1.26.1 → splent_cli-1.27.1}/src/splent_cli/services/release_dry_run.py +20 -6
  125. {splent_cli-1.26.1 → splent_cli-1.27.1}/src/splent_cli/services/release_gate.py +48 -12
  126. {splent_cli-1.26.1 → splent_cli-1.27.1}/src/splent_cli/services/spl_store.py +353 -61
  127. splent_cli-1.27.1/src/splent_cli/utils/archetype.py +245 -0
  128. splent_cli-1.27.1/src/splent_cli/utils/cache_utils.py +203 -0
  129. {splent_cli-1.26.1 → splent_cli-1.27.1}/src/splent_cli/utils/command_loader.py +14 -2
  130. {splent_cli-1.26.1 → splent_cli-1.27.1}/src/splent_cli/utils/contract_freshness.py +61 -4
  131. {splent_cli-1.26.1 → splent_cli-1.27.1}/src/splent_cli/utils/dynamic_imports.py +29 -57
  132. {splent_cli-1.26.1 → splent_cli-1.27.1}/src/splent_cli/utils/feature_utils.py +124 -17
  133. {splent_cli-1.26.1 → splent_cli-1.27.1}/src/splent_cli/utils/forge.py +90 -5
  134. {splent_cli-1.26.1 → splent_cli-1.27.1}/src/splent_cli/utils/git_url.py +117 -10
  135. splent_cli-1.27.1/src/splent_cli/utils/integrity.py +433 -0
  136. {splent_cli-1.26.1 → splent_cli-1.27.1}/src/splent_cli/utils/io_utils.py +140 -14
  137. {splent_cli-1.26.1 → splent_cli-1.27.1}/src/splent_cli/utils/lifecycle.py +85 -21
  138. {splent_cli-1.26.1 → splent_cli-1.27.1}/src/splent_cli/utils/manifest.py +47 -14
  139. {splent_cli-1.26.1 → splent_cli-1.27.1}/src/splent_cli/utils/proc.py +48 -3
  140. splent_cli-1.27.1/src/splent_cli/utils/redact.py +62 -0
  141. {splent_cli-1.26.1 → splent_cli-1.27.1}/src/splent_cli/utils/template_drift.py +75 -0
  142. {splent_cli-1.26.1 → splent_cli-1.27.1/src/splent_cli.egg-info}/PKG-INFO +2 -2
  143. {splent_cli-1.26.1 → splent_cli-1.27.1}/src/splent_cli.egg-info/SOURCES.txt +1 -0
  144. {splent_cli-1.26.1 → splent_cli-1.27.1}/src/splent_cli.egg-info/requires.txt +1 -1
  145. splent_cli-1.26.1/src/splent_cli/commands/cache/cache_clear.py +0 -88
  146. splent_cli-1.26.1/src/splent_cli/commands/cache/cache_orphans.py +0 -98
  147. splent_cli-1.26.1/src/splent_cli/commands/cache/cache_prune.py +0 -123
  148. splent_cli-1.26.1/src/splent_cli/commands/cache/cache_versions.py +0 -47
  149. splent_cli-1.26.1/src/splent_cli/commands/clear/clear_uploads.py +0 -42
  150. splent_cli-1.26.1/src/splent_cli/commands/command/command_create.py +0 -60
  151. splent_cli-1.26.1/src/splent_cli/commands/env/env_set.py +0 -174
  152. splent_cli-1.26.1/src/splent_cli/commands/env/env_show.py +0 -62
  153. splent_cli-1.26.1/src/splent_cli/commands/feature/feature_clean.py +0 -347
  154. splent_cli-1.26.1/src/splent_cli/commands/feature/feature_drift.py +0 -145
  155. splent_cli-1.26.1/src/splent_cli/commands/feature/feature_hook_add.py +0 -186
  156. splent_cli-1.26.1/src/splent_cli/commands/feature/feature_inject_config.py +0 -216
  157. splent_cli-1.26.1/src/splent_cli/commands/locust.py +0 -140
  158. splent_cli-1.26.1/src/splent_cli/commands/product/product_down.py +0 -140
  159. splent_cli-1.26.1/src/splent_cli/commands/release/release_resume.py +0 -436
  160. splent_cli-1.26.1/src/splent_cli/commands/spl/spl_pin.py +0 -89
  161. splent_cli-1.26.1/src/splent_cli/commands/spl/spl_remove_feature.py +0 -184
  162. splent_cli-1.26.1/src/splent_cli/utils/__init__.py +0 -0
  163. splent_cli-1.26.1/src/splent_cli/utils/archetype.py +0 -56
  164. splent_cli-1.26.1/src/splent_cli/utils/cache_utils.py +0 -115
  165. splent_cli-1.26.1/src/splent_cli/utils/integrity.py +0 -209
  166. {splent_cli-1.26.1 → splent_cli-1.27.1}/LICENSE +0 -0
  167. {splent_cli-1.26.1 → splent_cli-1.27.1}/README.md +0 -0
  168. {splent_cli-1.26.1 → splent_cli-1.27.1}/setup.cfg +0 -0
  169. {splent_cli-1.26.1 → splent_cli-1.27.1}/src/splent_cli/__main__.py +0 -0
  170. {splent_cli-1.26.1/src/splent_cli → splent_cli-1.27.1/src/splent_cli/commands}/__init__.py +0 -0
  171. {splent_cli-1.26.1/src/splent_cli/commands → splent_cli-1.27.1/src/splent_cli/commands/cache}/__init__.py +0 -0
  172. {splent_cli-1.26.1 → splent_cli-1.27.1}/src/splent_cli/commands/cache/cache_size.py +0 -0
  173. {splent_cli-1.26.1/src/splent_cli/commands/cache → splent_cli-1.27.1/src/splent_cli/commands/check}/__init__.py +0 -0
  174. {splent_cli-1.26.1 → splent_cli-1.27.1}/src/splent_cli/commands/check/check_docker.py +0 -0
  175. {splent_cli-1.26.1/src/splent_cli/commands/check → splent_cli-1.27.1/src/splent_cli/commands/clear}/__init__.py +0 -0
  176. {splent_cli-1.26.1 → splent_cli-1.27.1}/src/splent_cli/commands/clear/clear_build.py +0 -0
  177. {splent_cli-1.26.1/src/splent_cli/commands/clear → splent_cli-1.27.1/src/splent_cli/commands/command}/__init__.py +0 -0
  178. {splent_cli-1.26.1/src/splent_cli/commands/command → splent_cli-1.27.1/src/splent_cli/commands/export}/__init__.py +0 -0
  179. {splent_cli-1.26.1 → splent_cli-1.27.1}/src/splent_cli/commands/feature/feature_discard.py +0 -0
  180. {splent_cli-1.26.1 → splent_cli-1.27.1}/src/splent_cli/commands/feature/feature_env.py +0 -0
  181. {splent_cli-1.26.1 → splent_cli-1.27.1}/src/splent_cli/commands/feature/feature_fork.py +0 -0
  182. {splent_cli-1.26.1 → splent_cli-1.27.1}/src/splent_cli/commands/feature/feature_git.py +0 -0
  183. {splent_cli-1.26.1 → splent_cli-1.27.1}/src/splent_cli/commands/feature/feature_hooks.py +0 -0
  184. {splent_cli-1.26.1 → splent_cli-1.27.1}/src/splent_cli/commands/feature/feature_impact.py +0 -0
  185. {splent_cli-1.26.1 → splent_cli-1.27.1}/src/splent_cli/commands/feature/feature_pull.py +0 -0
  186. {splent_cli-1.26.1 → splent_cli-1.27.1}/src/splent_cli/commands/marketplace/marketplace_auth_utils.py +0 -0
  187. {splent_cli-1.26.1 → splent_cli-1.27.1}/src/splent_cli/commands/marketplace/whoami.py +0 -0
  188. {splent_cli-1.26.1/src/splent_cli/commands/export → splent_cli-1.27.1/src/splent_cli/commands/product}/__init__.py +0 -0
  189. {splent_cli-1.26.1 → splent_cli-1.27.1}/src/splent_cli/commands/product/product_commands.py +0 -0
  190. {splent_cli-1.26.1 → splent_cli-1.27.1}/src/splent_cli/commands/product/product_configure.py +0 -0
  191. {splent_cli-1.26.1 → splent_cli-1.27.1}/src/splent_cli/commands/product/product_console.py +0 -0
  192. {splent_cli-1.26.1 → splent_cli-1.27.1}/src/splent_cli/commands/product/product_deselect.py +0 -0
  193. {splent_cli-1.26.1 → splent_cli-1.27.1}/src/splent_cli/commands/product/product_list.py +0 -0
  194. {splent_cli-1.26.1 → splent_cli-1.27.1}/src/splent_cli/commands/product/product_logs.py +0 -0
  195. {splent_cli-1.26.1 → splent_cli-1.27.1}/src/splent_cli/commands/product/product_routes.py +0 -0
  196. {splent_cli-1.26.1 → splent_cli-1.27.1}/src/splent_cli/commands/product/product_signals.py +0 -0
  197. {splent_cli-1.26.1 → splent_cli-1.27.1}/src/splent_cli/commands/product/product_validate.py +0 -0
  198. {splent_cli-1.26.1/src/splent_cli/commands/product → splent_cli-1.27.1/src/splent_cli/commands/release}/__init__.py +0 -0
  199. {splent_cli-1.26.1/src/splent_cli/commands/release → splent_cli-1.27.1/src/splent_cli/commands/spl}/__init__.py +0 -0
  200. {splent_cli-1.26.1 → splent_cli-1.27.1}/src/splent_cli/commands/spl/spl_configurations.py +0 -0
  201. {splent_cli-1.26.1 → splent_cli-1.27.1}/src/splent_cli/commands/spl/spl_features.py +0 -0
  202. {splent_cli-1.26.1 → splent_cli-1.27.1}/src/splent_cli/commands/spl/spl_fetch.py +0 -0
  203. {splent_cli-1.26.1 → splent_cli-1.27.1}/src/splent_cli/commands/spl/spl_info.py +0 -0
  204. {splent_cli-1.26.1 → splent_cli-1.27.1}/src/splent_cli/commands/spl/spl_list.py +0 -0
  205. {splent_cli-1.26.1 → splent_cli-1.27.1}/src/splent_cli/commands/spl/spl_utils.py +0 -0
  206. {splent_cli-1.26.1 → splent_cli-1.27.1}/src/splent_cli/commands/tokens_setup.py +0 -0
  207. {splent_cli-1.26.1/src/splent_cli/commands/spl → splent_cli-1.27.1/src/splent_cli/services}/__init__.py +0 -0
  208. {splent_cli-1.26.1 → splent_cli-1.27.1}/src/splent_cli/services/index_refresh.py +0 -0
  209. {splent_cli-1.26.1 → splent_cli-1.27.1}/src/splent_cli/services/pypi_quota.py +0 -0
  210. {splent_cli-1.26.1/src/splent_cli/services → splent_cli-1.27.1/src/splent_cli/utils}/__init__.py +0 -0
  211. {splent_cli-1.26.1 → splent_cli-1.27.1}/src/splent_cli/utils/db_utils.py +0 -0
  212. {splent_cli-1.26.1 → splent_cli-1.27.1}/src/splent_cli/utils/decorators.py +0 -0
  213. {splent_cli-1.26.1 → splent_cli-1.27.1}/src/splent_cli/utils/feature_installer.py +0 -0
  214. {splent_cli-1.26.1 → splent_cli-1.27.1}/src/splent_cli/utils/path_utils.py +0 -0
  215. {splent_cli-1.26.1 → splent_cli-1.27.1}/src/splent_cli.egg-info/dependency_links.txt +0 -0
  216. {splent_cli-1.26.1 → splent_cli-1.27.1}/src/splent_cli.egg-info/entry_points.txt +0 -0
  217. {splent_cli-1.26.1 → splent_cli-1.27.1}/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.26.1
3
+ Version: 1.27.1
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.1
17
+ Requires-Dist: splent_framework<2,>=1.16.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.26.1"
7
+ version = "1.27.1"
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.1,<2",
27
+ "splent_framework>=1.16.1,<2",
28
28
  "tomli_w>=1.0.0",
29
29
  ]
30
30
 
@@ -0,0 +1,15 @@
1
+ """The SPLENT command-line interface."""
2
+
3
+ import os as _os
4
+ from types import MappingProxyType as _MappingProxyType
5
+
6
+ # What the operator actually exported, taken before anything reads a .env.
7
+ #
8
+ # This must stay the first thing the package does. cli.py runs load_dotenv(),
9
+ # and so do framework modules it imports (path_utils, configuration), which
10
+ # puts every workspace .env key into os.environ before any command runs. Read
11
+ # afterwards, the shell and the file look identical: env:show could never say
12
+ # "not loaded", env:list never showed "·", and the workspace .env passed for
13
+ # shell values and beat the product's docker/.env. Compare against this
14
+ # instead of os.environ whenever the question is "what did the shell export".
15
+ SHELL_ENV = _MappingProxyType(dict(_os.environ))
@@ -6,6 +6,8 @@ from dotenv import load_dotenv
6
6
 
7
7
  from splent_cli.utils.command_loader import load_commands
8
8
 
9
+ # From here on os.environ holds the workspace .env as well. What the shell
10
+ # itself exported is splent_cli.SHELL_ENV, taken before this line ran.
9
11
  load_dotenv()
10
12
 
11
13
 
@@ -0,0 +1,208 @@
1
+ from splent_cli.services import context, compose
2
+ import re
3
+ import click
4
+ from pathlib import Path
5
+
6
+ from splent_cli.utils.cache_utils import rmtree_force
7
+ from splent_cli.utils.feature_utils import normalize_namespace, parse_feature_entry
8
+
9
+ # A namespace, feature name or version is a single path component: it can
10
+ # never be empty, a path, "." or "..", so a scope always stays inside
11
+ # .splent_cache/features.
12
+ _NAME_RE = re.compile(r"^[A-Za-z0-9_][A-Za-z0-9_.-]*$")
13
+ _VERSION_RE = re.compile(r"^[A-Za-z0-9_][A-Za-z0-9_.+-]*$")
14
+
15
+
16
+ def _plain(value: str, pattern=_NAME_RE) -> bool:
17
+ return bool(pattern.match(value)) and ".." not in value
18
+
19
+
20
+ def _validate_namespace(ctx, param, value):
21
+ if value is None:
22
+ return None
23
+ if not _plain(value):
24
+ raise click.BadParameter(
25
+ "must be a namespace name such as splent_io or splent-io,"
26
+ " not empty and not a path."
27
+ )
28
+ return normalize_namespace(value)
29
+
30
+
31
+ def _validate_feature(ctx, param, value):
32
+ if value is None:
33
+ return None
34
+ base, at, version = value.partition("@")
35
+ explicit_ns, slash, name = base.rpartition("/")
36
+ if (
37
+ not _plain(name)
38
+ or (slash and not _plain(explicit_ns))
39
+ or (at and not _plain(version, _VERSION_RE))
40
+ ):
41
+ raise click.BadParameter(
42
+ "must be a feature name such as splent_feature_auth, optionally"
43
+ " with a namespace and a version (splent-io/splent_feature_auth@v1.2.0)."
44
+ )
45
+ ns, name, version = parse_feature_entry(value)
46
+ return {
47
+ "raw": value,
48
+ "namespace": ns if slash else None,
49
+ "name": name,
50
+ "version": version,
51
+ }
52
+
53
+
54
+ def _matches(entry_name: str, name: str, version: str | None) -> bool:
55
+ if version:
56
+ return entry_name == f"{name}@{version}"
57
+ return entry_name.split("@", 1)[0] == name
58
+
59
+
60
+ def _delete(target: Path, cache_root: Path) -> None:
61
+ """Delete one cache entry or namespace, refusing anything that is not a
62
+ real directory directly under ``cache_root`` or one of its namespaces.
63
+
64
+ Raises ``OSError`` when the target is refused or cannot be removed.
65
+ """
66
+ root = cache_root.resolve()
67
+ parent = target.parent.resolve()
68
+ if (
69
+ target.is_symlink()
70
+ or not target.is_dir()
71
+ or target.resolve().parent != parent
72
+ or (parent != root and parent.parent != root)
73
+ ):
74
+ raise OSError(
75
+ f"refusing to delete {target}, it is not a directory inside {cache_root}"
76
+ )
77
+ rmtree_force(target)
78
+
79
+
80
+ @click.command(
81
+ "cache:clear",
82
+ short_help="Clear the feature version cache (.splent_cache), totally or partially.",
83
+ )
84
+ @click.option(
85
+ "--namespace",
86
+ default=None,
87
+ callback=_validate_namespace,
88
+ help="Clear only a specific namespace (e.g. splent_io or splent-io).",
89
+ )
90
+ @click.option(
91
+ "--feature",
92
+ default=None,
93
+ callback=_validate_feature,
94
+ help=(
95
+ "Clear the entries of one feature, given as a bare name (all versions,"
96
+ " editable included), name@version or namespace/name[@version]."
97
+ ),
98
+ )
99
+ @click.option("--yes", is_flag=True, help="Skip confirmation prompt.")
100
+ def cache_clear(namespace, feature, yes):
101
+ """
102
+ Deletes entries from the feature cache and removes broken symlinks in products.
103
+
104
+ \b
105
+ Scope (most to least specific):
106
+ --feature splent_feature_auth → every entry of that feature
107
+ --feature splent_feature_auth@v1.2.0 → only that version
108
+ --feature splent-io/splent_feature_auth → that feature in one namespace
109
+ --namespace splent_io → the entire namespace folder
110
+ (no options) → the entire feature cache
111
+ """
112
+ if feature is not None and feature["namespace"] is not None:
113
+ if namespace is not None and namespace != feature["namespace"]:
114
+ raise click.BadParameter(
115
+ f"names namespace '{feature['namespace']}' but --namespace"
116
+ f" is '{namespace}'.",
117
+ param_hint="'--feature'",
118
+ )
119
+ namespace = feature["namespace"]
120
+
121
+ workspace = context.workspace()
122
+ cache_root = workspace / ".splent_cache" / "features"
123
+
124
+ if not cache_root.is_dir():
125
+ click.secho("⚠️ No .splent_cache/features directory found.", fg="yellow")
126
+ raise SystemExit(0)
127
+
128
+ # Resolve targets
129
+ if feature is not None:
130
+ targets = []
131
+ if namespace is not None:
132
+ search_in = [cache_root / namespace]
133
+ else:
134
+ search_in = sorted(cache_root.iterdir())
135
+ for ns_dir in search_in:
136
+ if ns_dir.is_symlink() or not ns_dir.is_dir():
137
+ continue
138
+ for feat_dir in sorted(ns_dir.iterdir()):
139
+ if feat_dir.is_dir() and _matches(
140
+ feat_dir.name, feature["name"], feature["version"]
141
+ ):
142
+ targets.append(feat_dir)
143
+ if not targets:
144
+ label = f"'{feature['raw']}'" + (
145
+ f" in namespace '{namespace}'" if namespace is not None else ""
146
+ )
147
+ click.secho(f"⚠️ No cache entries found for {label}.", fg="yellow")
148
+ raise SystemExit(0)
149
+ description = f"{len(targets)} entry/entries for feature '{feature['raw']}'"
150
+ elif namespace is not None:
151
+ targets = [cache_root / namespace]
152
+ if not targets[0].is_dir():
153
+ click.secho(f"⚠️ Namespace '{namespace}' not found in cache.", fg="yellow")
154
+ raise SystemExit(0)
155
+ description = f"namespace '{namespace}'"
156
+ else:
157
+ targets = sorted(cache_root.iterdir())
158
+ description = "entire feature cache"
159
+
160
+ if not yes and not click.confirm(
161
+ f"⚠️ This will permanently delete the {description}. Continue?"
162
+ ):
163
+ click.echo("❎ Cancelled.")
164
+ raise SystemExit(0)
165
+
166
+ # Delete, one entry at a time so a failure never stops the cleanup.
167
+ failures = []
168
+ for t in targets:
169
+ try:
170
+ if (
171
+ feature is None
172
+ and namespace is None
173
+ and (t.is_symlink() or not t.is_dir())
174
+ ):
175
+ # Stray file or link directly in features/: unlink, never follow.
176
+ t.unlink()
177
+ else:
178
+ _delete(t, cache_root)
179
+ except OSError as exc:
180
+ failures.append(t)
181
+ click.secho(
182
+ f" Could not delete {t.relative_to(cache_root)}: {exc}",
183
+ fg="red",
184
+ err=True,
185
+ )
186
+ cache_root.mkdir(parents=True, exist_ok=True)
187
+ if not failures:
188
+ click.secho(f"🧹 Cleared: {description}.", fg="green")
189
+
190
+ # Clean broken symlinks
191
+ removed = compose.remove_broken_symlinks(workspace)
192
+ if removed:
193
+ click.secho(f"🔗 Removed {removed} broken feature symlink(s).", fg="yellow")
194
+ else:
195
+ click.secho("✅ No broken symlinks found.", fg="green")
196
+
197
+ if failures:
198
+ click.secho(
199
+ f"{len(failures)} cache entry/entries could not be deleted. Check the"
200
+ " filesystem permissions (files written by a container may belong"
201
+ " to root) and remove them by hand.",
202
+ fg="red",
203
+ err=True,
204
+ )
205
+ raise SystemExit(1)
206
+
207
+
208
+ cli_command = cache_clear
@@ -0,0 +1,58 @@
1
+ from splent_cli.services import context
2
+ import click
3
+ from pathlib import Path
4
+
5
+ from splent_cli.utils.cache_utils import (
6
+ cache_entries,
7
+ cache_entry_key,
8
+ product_ref_keys,
9
+ )
10
+ from splent_cli.utils.feature_utils import workspace_feature_entries
11
+
12
+
13
+ def _get_cache_entries(cache_root: Path) -> list:
14
+ """Returns list of {namespace, name, version, is_versioned, path} dicts."""
15
+ return cache_entries(cache_root)
16
+
17
+
18
+ def _get_all_product_refs(workspace: Path) -> set:
19
+ """Every ``(namespace, name, version)`` any product declares, in any
20
+ environment list.
21
+
22
+ The namespace is normalised and a bare entry belongs to the default
23
+ namespace, so the keys compare directly with :func:`cache_entry_key`.
24
+ """
25
+ return product_ref_keys(workspace_feature_entries(workspace))
26
+
27
+
28
+ @click.command(
29
+ "cache:orphans", short_help="Show cached features not referenced by any product."
30
+ )
31
+ def cache_orphans():
32
+ """Lists features in the cache that no product references in its pyproject.toml."""
33
+ workspace = context.workspace()
34
+ cache_root = workspace / ".splent_cache" / "features"
35
+
36
+ entries = _get_cache_entries(cache_root)
37
+ if not entries:
38
+ click.secho("ℹ️ Feature cache is empty.", fg="yellow")
39
+ return
40
+
41
+ refs = _get_all_product_refs(workspace)
42
+
43
+ orphans = []
44
+ for e in entries:
45
+ if cache_entry_key(e) not in refs:
46
+ ref_key = f"{e['name']}@{e['version']}" if e["is_versioned"] else e["name"]
47
+ orphans.append(f"{e['namespace']}/{ref_key}")
48
+
49
+ if not orphans:
50
+ click.secho("✅ No orphaned features in cache.", fg="green")
51
+ return
52
+
53
+ click.secho(f"🗑 Orphaned cache entries ({len(orphans)}):", fg="yellow")
54
+ for o in orphans:
55
+ click.echo(f" - {o}")
56
+
57
+
58
+ cli_command = cache_orphans
@@ -1,13 +1,19 @@
1
1
  from splent_cli.services import context
2
- import re
3
2
  import click
4
3
  from pathlib import Path
5
4
  from collections import defaultdict
6
- from packaging.version import Version, InvalidVersion
5
+ from packaging.version import Version
6
+
7
+ from splent_cli.commands.cache.cache_versions import parse_version, sort_versions
8
+ from splent_cli.utils.feature_utils import (
9
+ normalize_namespace,
10
+ parse_feature_entry,
11
+ workspace_feature_entries,
12
+ )
7
13
 
8
14
 
9
15
  def _get_cache_versions(cache_root: Path) -> dict:
10
- """Returns {name: [version, ...]} with all versioned snapshots in cache."""
16
+ """Returns {(namespace, name): [version, ...]} with all versioned snapshots in cache."""
11
17
  versions = defaultdict(list)
12
18
  if not cache_root.exists():
13
19
  return versions
@@ -18,51 +24,36 @@ def _get_cache_versions(cache_root: Path) -> dict:
18
24
  if not feat_dir.is_dir() or "@" not in feat_dir.name:
19
25
  continue
20
26
  name, version = feat_dir.name.split("@", 1)
21
- versions[name].append(version)
27
+ versions[(normalize_namespace(ns_dir.name), name)].append(version)
22
28
  return versions
23
29
 
24
30
 
25
31
  def _latest(versions: list) -> str:
26
- """Returns the latest version string, using semver if possible."""
32
+ """Returns the latest version string, using semver if possible.
33
+
34
+ Valid versions beat unparseable ones; ties are broken by the raw string,
35
+ so the result never depends on the order of ``versions``.
36
+ """
27
37
 
28
38
  def sort_key(v):
29
- try:
30
- return Version(v.lstrip("v"))
31
- except InvalidVersion:
32
- return Version("0")
39
+ parsed = parse_version(v)
40
+ return (parsed is not None, parsed or Version("0"), v)
33
41
 
34
42
  return max(versions, key=sort_key)
35
43
 
36
44
 
37
45
  def _get_product_features(workspace: Path) -> dict:
38
- """Returns {product_name: {feature_name: version_or_None}}."""
46
+ """Returns {product_name: {(namespace, feature_name): version_or_None}}.
47
+
48
+ Every environment list counts (features, features_dev, features_prod).
49
+ """
39
50
  products = {}
40
- for product_dir in sorted(workspace.iterdir()):
41
- if not product_dir.is_dir() or product_dir.name.startswith("."):
42
- continue
43
- pyproject = product_dir / "pyproject.toml"
44
- if not pyproject.exists():
45
- continue
46
- content = pyproject.read_text()
47
- m = re.search(
48
- r"\[project\.optional-dependencies\].*?features\s*=\s*\[(.*?)\]",
49
- content,
50
- re.DOTALL,
51
- )
52
- if not m:
53
- continue
51
+ for product, entries in workspace_feature_entries(workspace).items():
54
52
  features = {}
55
- for raw in re.findall(r'"([^"]+)"|\'([^\']+)\'', m.group(1)):
56
- ref = raw[0] or raw[1]
57
- if "/" in ref:
58
- ref = ref.split("/", 1)[1]
59
- if "@" in ref:
60
- name, version = ref.split("@", 1)
61
- features[name] = version
62
- else:
63
- features[ref] = None
64
- if features:
65
- products[product_dir.name] = features
53
+ for entry in entries:
54
+ ns, name, version = parse_feature_entry(entry)
55
+ features[(ns, name)] = version
56
+ products[product] = features
66
57
  return products
67
58
 
68
59
 
@@ -90,8 +81,9 @@ def cache_outdated():
90
81
 
91
82
  outdated = []
92
83
  for product, features in sorted(products.items()):
93
- for name, current_ver in features.items():
94
- available = cache_versions.get(name)
84
+ for (ns, feat), current_ver in features.items():
85
+ name = f"{ns}/{feat}"
86
+ available = cache_versions.get((ns, feat))
95
87
  if not available:
96
88
  continue
97
89
  if current_ver is None:
@@ -101,9 +93,11 @@ def cache_outdated():
101
93
  )
102
94
  continue
103
95
  latest = _latest(available)
104
- try:
105
- is_old = Version(latest.lstrip("v")) > Version(current_ver.lstrip("v"))
106
- except InvalidVersion:
96
+ latest_parsed = parse_version(latest)
97
+ current_parsed = parse_version(current_ver)
98
+ if latest_parsed is not None and current_parsed is not None:
99
+ is_old = latest_parsed > current_parsed
100
+ else:
107
101
  is_old = latest != current_ver
108
102
  if is_old:
109
103
  outdated.append((product, name, current_ver, latest, available))
@@ -118,7 +112,7 @@ def cache_outdated():
118
112
  f" {click.style(product, bold=True)} {name} "
119
113
  f"{click.style(current, fg='red')} → {click.style(latest, fg='green')}"
120
114
  )
121
- others = sorted(set(available) - {latest})
115
+ others = sort_versions(set(available) - {latest})
122
116
  if others:
123
117
  click.echo(f" also cached: {', '.join(others)}")
124
118
  click.echo()
@@ -0,0 +1,148 @@
1
+ from splent_cli.services import context, compose
2
+ import click
3
+ import os
4
+ import tomllib
5
+ from pathlib import Path
6
+
7
+ from splent_cli.utils.cache_utils import (
8
+ cache_entries,
9
+ cache_entry_key,
10
+ product_ref_keys,
11
+ rmtree_force,
12
+ )
13
+ from splent_cli.utils.feature_utils import (
14
+ load_product_pyproject,
15
+ read_all_feature_entries,
16
+ )
17
+
18
+
19
+ def _get_cache_entries(cache_root: Path) -> list:
20
+ return cache_entries(cache_root)
21
+
22
+
23
+ def _scan_product_entries(workspace: Path) -> tuple[dict, list]:
24
+ """Strict variant of ``workspace_feature_entries`` for a destructive command.
25
+
26
+ Returns ``({product: entries}, [(directory, error), ...])``. A non-hidden
27
+ directory holding a pyproject.toml that cannot be read or parsed is a
28
+ failure, not a product to skip: it may pin entries that would otherwise
29
+ look orphaned. Directories without a pyproject.toml are ignored.
30
+ """
31
+ products: dict[str, list[str]] = {}
32
+ failures: list[tuple[str, str]] = []
33
+ try:
34
+ children = sorted(os.scandir(workspace), key=lambda e: e.name)
35
+ except OSError as e:
36
+ return products, [(str(workspace), str(e))]
37
+ for child in children:
38
+ if not child.is_dir() or child.name.startswith("."):
39
+ continue
40
+ try:
41
+ data = load_product_pyproject(child.path)
42
+ except FileNotFoundError:
43
+ continue
44
+ except (OSError, tomllib.TOMLDecodeError) as e:
45
+ failures.append((child.name, str(e)))
46
+ continue
47
+ entries = read_all_feature_entries(data)
48
+ if entries:
49
+ products[child.name] = entries
50
+ return products, failures
51
+
52
+
53
+ def _get_all_product_refs(workspace: Path) -> set:
54
+ """Every ``(namespace, name, version)`` any product declares.
55
+
56
+ Raises ``SystemExit(1)`` when a product's pyproject.toml cannot be read.
57
+ """
58
+ products, failures = _scan_product_entries(workspace)
59
+ if failures:
60
+ click.secho(
61
+ "cache:prune stopped because these pyproject.toml files could not be read.",
62
+ fg="red",
63
+ err=True,
64
+ )
65
+ for directory, error in failures:
66
+ click.secho(f" - {directory}/pyproject.toml ({error})", fg="red", err=True)
67
+ click.secho(
68
+ " Every entry they pin would look orphaned. Fix them and run"
69
+ " cache:prune again. Nothing was deleted.",
70
+ fg="red",
71
+ err=True,
72
+ )
73
+ raise SystemExit(1)
74
+ return product_ref_keys(products)
75
+
76
+
77
+ @click.command(
78
+ "cache:prune", short_help="Remove orphaned cache entries not used by any product."
79
+ )
80
+ @click.option("--yes", is_flag=True, help="Skip confirmation prompt.")
81
+ def cache_prune(yes):
82
+ """
83
+ Removes cache entries that no product references in its pyproject.toml,
84
+ then cleans up broken symlinks in products.
85
+
86
+ Stops without deleting anything when a product's pyproject.toml cannot
87
+ be parsed.
88
+ """
89
+ workspace = context.workspace()
90
+ cache_root = workspace / ".splent_cache" / "features"
91
+
92
+ entries = _get_cache_entries(cache_root)
93
+ if not entries:
94
+ click.secho("ℹ️ Feature cache is empty.", fg="yellow")
95
+ return
96
+
97
+ refs = _get_all_product_refs(workspace)
98
+
99
+ orphans = [e for e in entries if cache_entry_key(e) not in refs]
100
+
101
+ if not orphans:
102
+ click.secho("✅ Nothing to prune — no orphaned entries.", fg="green")
103
+ return
104
+
105
+ click.secho(f"Orphaned entries to remove ({len(orphans)}):", fg="yellow")
106
+ for e in orphans:
107
+ click.echo(f" - {_label(e)}")
108
+
109
+ click.echo()
110
+ if not yes and not click.confirm("Remove all of the above?"):
111
+ click.echo("❎ Cancelled.")
112
+ raise SystemExit(0)
113
+
114
+ failures = []
115
+ for e in orphans:
116
+ try:
117
+ rmtree_force(e["path"])
118
+ except OSError as exc:
119
+ failures.append((e, exc))
120
+ click.secho(f" Could not delete {_label(e)}: {exc}", fg="red", err=True)
121
+
122
+ pruned = len(orphans) - len(failures)
123
+ if pruned:
124
+ click.secho(f"🧹 Pruned {pruned} orphaned cache entry/entries.", fg="green")
125
+
126
+ removed = compose.remove_broken_symlinks(workspace)
127
+ if removed:
128
+ click.secho(f"🔗 Removed {removed} broken feature symlink(s).", fg="yellow")
129
+ else:
130
+ click.secho("✅ No broken symlinks found.", fg="green")
131
+
132
+ if failures:
133
+ click.secho(
134
+ f"{len(failures)} cache entry/entries could not be deleted. Check the"
135
+ " filesystem permissions (files written by a container may belong"
136
+ " to root) and remove them by hand.",
137
+ fg="red",
138
+ err=True,
139
+ )
140
+ raise SystemExit(1)
141
+
142
+
143
+ def _label(e: dict) -> str:
144
+ version = f"@{e['version']}" if e["is_versioned"] else " (editable)"
145
+ return f"{e['namespace']}/{e['name']}{version}"
146
+
147
+
148
+ cli_command = cache_prune
@@ -1,4 +1,6 @@
1
1
  from splent_cli.services import context
2
+ from splent_cli.commands.cache.cache_versions import sort_versions
3
+ from splent_cli.utils.feature_utils import editable_feature_dirs
2
4
  import click
3
5
  from pathlib import Path
4
6
  from collections import defaultdict
@@ -27,24 +29,8 @@ def _get_cache_grouped(cache_root: Path) -> dict:
27
29
  def _get_workspace_root_features(workspace: Path) -> dict:
28
30
  """Returns {namespace/name: ['workspace']} for editable features at workspace root."""
29
31
  grouped = defaultdict(list)
30
- for entry in sorted(workspace.iterdir()):
31
- if not entry.is_dir() or not entry.name.startswith("splent_feature_"):
32
- continue
33
- # Must have pyproject.toml to be a real feature
34
- if not (entry / "pyproject.toml").exists():
35
- continue
36
- src = entry / "src"
37
- if not src.is_dir():
38
- continue
39
- for ns_dir in src.iterdir():
40
- if (
41
- ns_dir.is_dir()
42
- and not ns_dir.name.startswith(("_", "."))
43
- and "." not in ns_dir.name
44
- and (ns_dir / entry.name).is_dir()
45
- ):
46
- grouped[f"{ns_dir.name}/{entry.name}"].append("workspace")
47
- break
32
+ for ns, name, _path in editable_feature_dirs(workspace):
33
+ grouped[f"{ns}/{name}"].append("workspace")
48
34
  return grouped
49
35
 
50
36
 
@@ -73,7 +59,7 @@ def cache_status():
73
59
 
74
60
  for feature, versions in sorted(grouped.items()):
75
61
  click.secho(f" {feature}", bold=True)
76
- sorted_versions = sorted(versions, key=lambda x: (x is not None, x or ""))
62
+ sorted_versions = sort_versions(versions)
77
63
  for i, v in enumerate(sorted_versions):
78
64
  connector = "└──" if i == len(sorted_versions) - 1 else "├──"
79
65
  if v == "workspace":