splent-cli 1.10.0__tar.gz → 1.12.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 (172) hide show
  1. {splent_cli-1.10.0/src/splent_cli.egg-info → splent_cli-1.12.0}/PKG-INFO +1 -1
  2. {splent_cli-1.10.0 → splent_cli-1.12.0}/pyproject.toml +6 -3
  3. {splent_cli-1.10.0 → splent_cli-1.12.0}/src/splent_cli/commands/check/check_infra.py +4 -12
  4. {splent_cli-1.10.0 → splent_cli-1.12.0}/src/splent_cli/commands/database/db_migrate.py +1 -3
  5. splent_cli-1.12.0/src/splent_cli/commands/database/db_reset.py +253 -0
  6. {splent_cli-1.10.0 → splent_cli-1.12.0}/src/splent_cli/commands/database/db_seed.py +1 -3
  7. {splent_cli-1.10.0 → splent_cli-1.12.0}/src/splent_cli/commands/doctor.py +3 -1
  8. {splent_cli-1.10.0 → splent_cli-1.12.0}/src/splent_cli/commands/feature/feature_clone.py +46 -99
  9. {splent_cli-1.10.0 → splent_cli-1.12.0}/src/splent_cli/commands/feature/feature_compile.py +1 -3
  10. {splent_cli-1.10.0 → splent_cli-1.12.0}/src/splent_cli/commands/feature/feature_env.py +1 -3
  11. {splent_cli-1.10.0 → splent_cli-1.12.0}/src/splent_cli/commands/feature/feature_install.py +21 -28
  12. {splent_cli-1.10.0 → splent_cli-1.12.0}/src/splent_cli/commands/feature/feature_refine.py +2 -5
  13. {splent_cli-1.10.0 → splent_cli-1.12.0}/src/splent_cli/commands/feature/feature_release.py +8 -1
  14. {splent_cli-1.10.0 → splent_cli-1.12.0}/src/splent_cli/commands/feature/feature_search.py +2 -6
  15. {splent_cli-1.10.0 → splent_cli-1.12.0}/src/splent_cli/commands/feature/feature_unlock.py +10 -2
  16. {splent_cli-1.10.0 → splent_cli-1.12.0}/src/splent_cli/commands/feature/feature_upgrade.py +30 -14
  17. {splent_cli-1.10.0 → splent_cli-1.12.0}/src/splent_cli/commands/lint.py +3 -1
  18. {splent_cli-1.10.0 → splent_cli-1.12.0}/src/splent_cli/commands/locust.py +2 -6
  19. {splent_cli-1.10.0 → splent_cli-1.12.0}/src/splent_cli/commands/product/product_clean.py +3 -1
  20. {splent_cli-1.10.0 → splent_cli-1.12.0}/src/splent_cli/commands/product/product_create.py +1 -7
  21. {splent_cli-1.10.0 → splent_cli-1.12.0}/src/splent_cli/commands/product/product_env.py +1 -1
  22. {splent_cli-1.10.0 → splent_cli-1.12.0}/src/splent_cli/commands/product/product_release.py +8 -1
  23. {splent_cli-1.10.0 → splent_cli-1.12.0}/src/splent_cli/commands/product/product_run.py +3 -1
  24. {splent_cli-1.10.0 → splent_cli-1.12.0}/src/splent_cli/commands/product/product_up.py +14 -3
  25. {splent_cli-1.10.0 → splent_cli-1.12.0}/src/splent_cli/commands/release/release_core.py +26 -4
  26. {splent_cli-1.10.0 → splent_cli-1.12.0}/src/splent_cli/commands/spl/spl_add_feature.py +34 -39
  27. splent_cli-1.12.0/src/splent_cli/commands/spl/spl_compare.py +88 -0
  28. {splent_cli-1.10.0 → splent_cli-1.12.0}/src/splent_cli/commands/spl/spl_create.py +4 -1
  29. {splent_cli-1.10.0 → splent_cli-1.12.0}/src/splent_cli/commands/uvl/uvl_utils.py +1 -3
  30. {splent_cli-1.10.0 → splent_cli-1.12.0}/src/splent_cli/services/release.py +112 -1
  31. {splent_cli-1.10.0 → splent_cli-1.12.0}/src/splent_cli/utils/dynamic_imports.py +1 -3
  32. splent_cli-1.12.0/src/splent_cli/utils/git_url.py +221 -0
  33. {splent_cli-1.10.0 → splent_cli-1.12.0}/src/splent_cli/utils/integrity.py +4 -1
  34. {splent_cli-1.10.0 → splent_cli-1.12.0}/src/splent_cli/utils/io_utils.py +3 -1
  35. {splent_cli-1.10.0 → splent_cli-1.12.0/src/splent_cli.egg-info}/PKG-INFO +1 -1
  36. {splent_cli-1.10.0 → splent_cli-1.12.0}/src/splent_cli.egg-info/SOURCES.txt +1 -0
  37. splent_cli-1.10.0/src/splent_cli/commands/database/db_reset.py +0 -159
  38. splent_cli-1.10.0/src/splent_cli/utils/git_url.py +0 -82
  39. {splent_cli-1.10.0 → splent_cli-1.12.0}/LICENSE +0 -0
  40. {splent_cli-1.10.0 → splent_cli-1.12.0}/README.md +0 -0
  41. {splent_cli-1.10.0 → splent_cli-1.12.0}/setup.cfg +0 -0
  42. {splent_cli-1.10.0 → splent_cli-1.12.0}/src/splent_cli/__init__.py +0 -0
  43. {splent_cli-1.10.0 → splent_cli-1.12.0}/src/splent_cli/__main__.py +0 -0
  44. {splent_cli-1.10.0 → splent_cli-1.12.0}/src/splent_cli/cli.py +0 -0
  45. {splent_cli-1.10.0 → splent_cli-1.12.0}/src/splent_cli/commands/__init__.py +0 -0
  46. {splent_cli-1.10.0 → splent_cli-1.12.0}/src/splent_cli/commands/cache/__init__.py +0 -0
  47. {splent_cli-1.10.0 → splent_cli-1.12.0}/src/splent_cli/commands/cache/cache_clear.py +0 -0
  48. {splent_cli-1.10.0 → splent_cli-1.12.0}/src/splent_cli/commands/cache/cache_orphans.py +0 -0
  49. {splent_cli-1.10.0 → splent_cli-1.12.0}/src/splent_cli/commands/cache/cache_outdated.py +0 -0
  50. {splent_cli-1.10.0 → splent_cli-1.12.0}/src/splent_cli/commands/cache/cache_prune.py +0 -0
  51. {splent_cli-1.10.0 → splent_cli-1.12.0}/src/splent_cli/commands/cache/cache_size.py +0 -0
  52. {splent_cli-1.10.0 → splent_cli-1.12.0}/src/splent_cli/commands/cache/cache_status.py +0 -0
  53. {splent_cli-1.10.0 → splent_cli-1.12.0}/src/splent_cli/commands/cache/cache_usage.py +0 -0
  54. {splent_cli-1.10.0 → splent_cli-1.12.0}/src/splent_cli/commands/cache/cache_versions.py +0 -0
  55. {splent_cli-1.10.0 → splent_cli-1.12.0}/src/splent_cli/commands/check/__init__.py +0 -0
  56. {splent_cli-1.10.0 → splent_cli-1.12.0}/src/splent_cli/commands/check/check_deps.py +0 -0
  57. {splent_cli-1.10.0 → splent_cli-1.12.0}/src/splent_cli/commands/check/check_docker.py +0 -0
  58. {splent_cli-1.10.0 → splent_cli-1.12.0}/src/splent_cli/commands/check/check_env.py +0 -0
  59. {splent_cli-1.10.0 → splent_cli-1.12.0}/src/splent_cli/commands/check/check_features.py +0 -0
  60. {splent_cli-1.10.0 → splent_cli-1.12.0}/src/splent_cli/commands/check/check_github.py +0 -0
  61. {splent_cli-1.10.0 → splent_cli-1.12.0}/src/splent_cli/commands/check/check_product.py +0 -0
  62. {splent_cli-1.10.0 → splent_cli-1.12.0}/src/splent_cli/commands/check/check_pypi.py +0 -0
  63. {splent_cli-1.10.0 → splent_cli-1.12.0}/src/splent_cli/commands/check/check_pyproject.py +0 -0
  64. {splent_cli-1.10.0 → splent_cli-1.12.0}/src/splent_cli/commands/clear/__init__.py +0 -0
  65. {splent_cli-1.10.0 → splent_cli-1.12.0}/src/splent_cli/commands/clear/clear_build.py +0 -0
  66. {splent_cli-1.10.0 → splent_cli-1.12.0}/src/splent_cli/commands/clear/clear_log.py +0 -0
  67. {splent_cli-1.10.0 → splent_cli-1.12.0}/src/splent_cli/commands/clear/clear_uploads.py +0 -0
  68. {splent_cli-1.10.0 → splent_cli-1.12.0}/src/splent_cli/commands/command/__init__.py +0 -0
  69. {splent_cli-1.10.0 → splent_cli-1.12.0}/src/splent_cli/commands/command/command_create.py +0 -0
  70. {splent_cli-1.10.0 → splent_cli-1.12.0}/src/splent_cli/commands/coverage.py +0 -0
  71. {splent_cli-1.10.0 → splent_cli-1.12.0}/src/splent_cli/commands/database/db_console.py +0 -0
  72. {splent_cli-1.10.0 → splent_cli-1.12.0}/src/splent_cli/commands/database/db_dump.py +0 -0
  73. {splent_cli-1.10.0 → splent_cli-1.12.0}/src/splent_cli/commands/database/db_restore.py +0 -0
  74. {splent_cli-1.10.0 → splent_cli-1.12.0}/src/splent_cli/commands/database/db_rollback.py +0 -0
  75. {splent_cli-1.10.0 → splent_cli-1.12.0}/src/splent_cli/commands/database/db_status.py +0 -0
  76. {splent_cli-1.10.0 → splent_cli-1.12.0}/src/splent_cli/commands/database/db_upgrade.py +0 -0
  77. {splent_cli-1.10.0 → splent_cli-1.12.0}/src/splent_cli/commands/env/env_list.py +0 -0
  78. {splent_cli-1.10.0 → splent_cli-1.12.0}/src/splent_cli/commands/env/env_set.py +0 -0
  79. {splent_cli-1.10.0 → splent_cli-1.12.0}/src/splent_cli/commands/env/env_show.py +0 -0
  80. {splent_cli-1.10.0 → splent_cli-1.12.0}/src/splent_cli/commands/export/__init__.py +0 -0
  81. {splent_cli-1.10.0 → splent_cli-1.12.0}/src/splent_cli/commands/export/export_puml.py +0 -0
  82. {splent_cli-1.10.0 → splent_cli-1.12.0}/src/splent_cli/commands/feature/feature_add.py +0 -0
  83. {splent_cli-1.10.0 → splent_cli-1.12.0}/src/splent_cli/commands/feature/feature_attach.py +0 -0
  84. {splent_cli-1.10.0 → splent_cli-1.12.0}/src/splent_cli/commands/feature/feature_clean.py +0 -0
  85. {splent_cli-1.10.0 → splent_cli-1.12.0}/src/splent_cli/commands/feature/feature_compat.py +0 -0
  86. {splent_cli-1.10.0 → splent_cli-1.12.0}/src/splent_cli/commands/feature/feature_contract.py +0 -0
  87. {splent_cli-1.10.0 → splent_cli-1.12.0}/src/splent_cli/commands/feature/feature_create.py +0 -0
  88. {splent_cli-1.10.0 → splent_cli-1.12.0}/src/splent_cli/commands/feature/feature_delete.py +0 -0
  89. {splent_cli-1.10.0 → splent_cli-1.12.0}/src/splent_cli/commands/feature/feature_detach.py +0 -0
  90. {splent_cli-1.10.0 → splent_cli-1.12.0}/src/splent_cli/commands/feature/feature_discard.py +0 -0
  91. {splent_cli-1.10.0 → splent_cli-1.12.0}/src/splent_cli/commands/feature/feature_drift.py +0 -0
  92. {splent_cli-1.10.0 → splent_cli-1.12.0}/src/splent_cli/commands/feature/feature_fork.py +0 -0
  93. {splent_cli-1.10.0 → splent_cli-1.12.0}/src/splent_cli/commands/feature/feature_git.py +0 -0
  94. {splent_cli-1.10.0 → splent_cli-1.12.0}/src/splent_cli/commands/feature/feature_hook_add.py +0 -0
  95. {splent_cli-1.10.0 → splent_cli-1.12.0}/src/splent_cli/commands/feature/feature_hook_remove.py +0 -0
  96. {splent_cli-1.10.0 → splent_cli-1.12.0}/src/splent_cli/commands/feature/feature_hooks.py +0 -0
  97. {splent_cli-1.10.0 → splent_cli-1.12.0}/src/splent_cli/commands/feature/feature_impact.py +0 -0
  98. {splent_cli-1.10.0 → splent_cli-1.12.0}/src/splent_cli/commands/feature/feature_inject_config.py +0 -0
  99. {splent_cli-1.10.0 → splent_cli-1.12.0}/src/splent_cli/commands/feature/feature_list.py +0 -0
  100. {splent_cli-1.10.0 → splent_cli-1.12.0}/src/splent_cli/commands/feature/feature_order.py +0 -0
  101. {splent_cli-1.10.0 → splent_cli-1.12.0}/src/splent_cli/commands/feature/feature_outdated.py +0 -0
  102. {splent_cli-1.10.0 → splent_cli-1.12.0}/src/splent_cli/commands/feature/feature_pin.py +0 -0
  103. {splent_cli-1.10.0 → splent_cli-1.12.0}/src/splent_cli/commands/feature/feature_pip_install.py +0 -0
  104. {splent_cli-1.10.0 → splent_cli-1.12.0}/src/splent_cli/commands/feature/feature_pull.py +0 -0
  105. {splent_cli-1.10.0 → splent_cli-1.12.0}/src/splent_cli/commands/feature/feature_remove.py +0 -0
  106. {splent_cli-1.10.0 → splent_cli-1.12.0}/src/splent_cli/commands/feature/feature_rename.py +0 -0
  107. {splent_cli-1.10.0 → splent_cli-1.12.0}/src/splent_cli/commands/feature/feature_status.py +0 -0
  108. {splent_cli-1.10.0 → splent_cli-1.12.0}/src/splent_cli/commands/feature/feature_sync_template.py +0 -0
  109. {splent_cli-1.10.0 → splent_cli-1.12.0}/src/splent_cli/commands/feature/feature_test.py +0 -0
  110. {splent_cli-1.10.0 → splent_cli-1.12.0}/src/splent_cli/commands/feature/feature_translate.py +0 -0
  111. {splent_cli-1.10.0 → splent_cli-1.12.0}/src/splent_cli/commands/feature/feature_versions.py +0 -0
  112. {splent_cli-1.10.0 → splent_cli-1.12.0}/src/splent_cli/commands/feature/feature_xray.py +0 -0
  113. {splent_cli-1.10.0 → splent_cli-1.12.0}/src/splent_cli/commands/product/__init__.py +0 -0
  114. {splent_cli-1.10.0 → splent_cli-1.12.0}/src/splent_cli/commands/product/product_auto_require.py +0 -0
  115. {splent_cli-1.10.0 → splent_cli-1.12.0}/src/splent_cli/commands/product/product_build.py +0 -0
  116. {splent_cli-1.10.0 → splent_cli-1.12.0}/src/splent_cli/commands/product/product_commands.py +0 -0
  117. {splent_cli-1.10.0 → splent_cli-1.12.0}/src/splent_cli/commands/product/product_config.py +0 -0
  118. {splent_cli-1.10.0 → splent_cli-1.12.0}/src/splent_cli/commands/product/product_configure.py +0 -0
  119. {splent_cli-1.10.0 → splent_cli-1.12.0}/src/splent_cli/commands/product/product_console.py +0 -0
  120. {splent_cli-1.10.0 → splent_cli-1.12.0}/src/splent_cli/commands/product/product_containers.py +0 -0
  121. {splent_cli-1.10.0 → splent_cli-1.12.0}/src/splent_cli/commands/product/product_deploy.py +0 -0
  122. {splent_cli-1.10.0 → splent_cli-1.12.0}/src/splent_cli/commands/product/product_derive.py +0 -0
  123. {splent_cli-1.10.0 → splent_cli-1.12.0}/src/splent_cli/commands/product/product_deselect.py +0 -0
  124. {splent_cli-1.10.0 → splent_cli-1.12.0}/src/splent_cli/commands/product/product_down.py +0 -0
  125. {splent_cli-1.10.0 → splent_cli-1.12.0}/src/splent_cli/commands/product/product_drift.py +0 -0
  126. {splent_cli-1.10.0 → splent_cli-1.12.0}/src/splent_cli/commands/product/product_list.py +0 -0
  127. {splent_cli-1.10.0 → splent_cli-1.12.0}/src/splent_cli/commands/product/product_logs.py +0 -0
  128. {splent_cli-1.10.0 → splent_cli-1.12.0}/src/splent_cli/commands/product/product_missing.py +0 -0
  129. {splent_cli-1.10.0 → splent_cli-1.12.0}/src/splent_cli/commands/product/product_port.py +0 -0
  130. {splent_cli-1.10.0 → splent_cli-1.12.0}/src/splent_cli/commands/product/product_resolve.py +0 -0
  131. {splent_cli-1.10.0 → splent_cli-1.12.0}/src/splent_cli/commands/product/product_restart.py +0 -0
  132. {splent_cli-1.10.0 → splent_cli-1.12.0}/src/splent_cli/commands/product/product_routes.py +0 -0
  133. {splent_cli-1.10.0 → splent_cli-1.12.0}/src/splent_cli/commands/product/product_select.py +0 -0
  134. {splent_cli-1.10.0 → splent_cli-1.12.0}/src/splent_cli/commands/product/product_shell.py +0 -0
  135. {splent_cli-1.10.0 → splent_cli-1.12.0}/src/splent_cli/commands/product/product_signals.py +0 -0
  136. {splent_cli-1.10.0 → splent_cli-1.12.0}/src/splent_cli/commands/product/product_sync_template.py +0 -0
  137. {splent_cli-1.10.0 → splent_cli-1.12.0}/src/splent_cli/commands/product/product_test.py +0 -0
  138. {splent_cli-1.10.0 → splent_cli-1.12.0}/src/splent_cli/commands/product/product_validate.py +0 -0
  139. {splent_cli-1.10.0 → splent_cli-1.12.0}/src/splent_cli/commands/release/__init__.py +0 -0
  140. {splent_cli-1.10.0 → splent_cli-1.12.0}/src/splent_cli/commands/selenium.py +0 -0
  141. {splent_cli-1.10.0 → splent_cli-1.12.0}/src/splent_cli/commands/spl/__init__.py +0 -0
  142. {splent_cli-1.10.0 → splent_cli-1.12.0}/src/splent_cli/commands/spl/spl_add_constraints.py +0 -0
  143. {splent_cli-1.10.0 → splent_cli-1.12.0}/src/splent_cli/commands/spl/spl_configurations.py +0 -0
  144. {splent_cli-1.10.0 → splent_cli-1.12.0}/src/splent_cli/commands/spl/spl_deps.py +0 -0
  145. {splent_cli-1.10.0 → splent_cli-1.12.0}/src/splent_cli/commands/spl/spl_features.py +0 -0
  146. {splent_cli-1.10.0 → splent_cli-1.12.0}/src/splent_cli/commands/spl/spl_fetch.py +0 -0
  147. {splent_cli-1.10.0 → splent_cli-1.12.0}/src/splent_cli/commands/spl/spl_info.py +0 -0
  148. {splent_cli-1.10.0 → splent_cli-1.12.0}/src/splent_cli/commands/spl/spl_list.py +0 -0
  149. {splent_cli-1.10.0 → splent_cli-1.12.0}/src/splent_cli/commands/spl/spl_utils.py +0 -0
  150. {splent_cli-1.10.0 → splent_cli-1.12.0}/src/splent_cli/commands/tokens_setup.py +0 -0
  151. {splent_cli-1.10.0 → splent_cli-1.12.0}/src/splent_cli/commands/version.py +0 -0
  152. {splent_cli-1.10.0 → splent_cli-1.12.0}/src/splent_cli/services/__init__.py +0 -0
  153. {splent_cli-1.10.0 → splent_cli-1.12.0}/src/splent_cli/services/compose.py +0 -0
  154. {splent_cli-1.10.0 → splent_cli-1.12.0}/src/splent_cli/services/context.py +0 -0
  155. {splent_cli-1.10.0 → splent_cli-1.12.0}/src/splent_cli/services/preflight.py +0 -0
  156. {splent_cli-1.10.0 → splent_cli-1.12.0}/src/splent_cli/utils/__init__.py +0 -0
  157. {splent_cli-1.10.0 → splent_cli-1.12.0}/src/splent_cli/utils/cache_utils.py +0 -0
  158. {splent_cli-1.10.0 → splent_cli-1.12.0}/src/splent_cli/utils/command_loader.py +0 -0
  159. {splent_cli-1.10.0 → splent_cli-1.12.0}/src/splent_cli/utils/contract_freshness.py +0 -0
  160. {splent_cli-1.10.0 → splent_cli-1.12.0}/src/splent_cli/utils/db_utils.py +0 -0
  161. {splent_cli-1.10.0 → splent_cli-1.12.0}/src/splent_cli/utils/decorators.py +0 -0
  162. {splent_cli-1.10.0 → splent_cli-1.12.0}/src/splent_cli/utils/feature_installer.py +0 -0
  163. {splent_cli-1.10.0 → splent_cli-1.12.0}/src/splent_cli/utils/feature_utils.py +0 -0
  164. {splent_cli-1.10.0 → splent_cli-1.12.0}/src/splent_cli/utils/lifecycle.py +0 -0
  165. {splent_cli-1.10.0 → splent_cli-1.12.0}/src/splent_cli/utils/manifest.py +0 -0
  166. {splent_cli-1.10.0 → splent_cli-1.12.0}/src/splent_cli/utils/path_utils.py +0 -0
  167. {splent_cli-1.10.0 → splent_cli-1.12.0}/src/splent_cli/utils/proc.py +0 -0
  168. {splent_cli-1.10.0 → splent_cli-1.12.0}/src/splent_cli/utils/template_drift.py +0 -0
  169. {splent_cli-1.10.0 → splent_cli-1.12.0}/src/splent_cli.egg-info/dependency_links.txt +0 -0
  170. {splent_cli-1.10.0 → splent_cli-1.12.0}/src/splent_cli.egg-info/entry_points.txt +0 -0
  171. {splent_cli-1.10.0 → splent_cli-1.12.0}/src/splent_cli.egg-info/requires.txt +0 -0
  172. {splent_cli-1.10.0 → splent_cli-1.12.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.10.0
3
+ Version: 1.12.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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "splent_cli"
7
- version = "1.10.0"
7
+ version = "1.12.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"
@@ -63,8 +63,11 @@ where = ["src"]
63
63
  testpaths = ["tests"]
64
64
  pythonpath = ["src"]
65
65
 
66
- [tool.black]
67
- line-length = 79
66
+ # Canonical formatter/linter is Ruff (`splent lint` runs `ruff check` +
67
+ # `ruff format`). Ruff's default line length (88) is the project standard;
68
+ # set explicitly here so it is unambiguous.
69
+ [tool.ruff]
70
+ line-length = 88
68
71
 
69
72
  [project.urls]
70
73
  Homepage = "https://github.com/diverso-lab/splent_cli"
@@ -209,9 +209,7 @@ def check_infra():
209
209
  click.echo(click.style(" Networks", bold=True))
210
210
  required_networks: set[str] = set()
211
211
  for label, cf in compose_files:
212
- result = _run(
213
- ["docker", "compose", "-f", cf, "config", "--format", "json"]
214
- )
212
+ result = _run(["docker", "compose", "-f", cf, "config", "--format", "json"])
215
213
  if result.returncode != 0:
216
214
  continue
217
215
  try:
@@ -242,9 +240,7 @@ def check_infra():
242
240
  build_count = 0
243
241
  for label, cf in compose_files:
244
242
  docker_dir = os.path.dirname(cf)
245
- result = _run(
246
- ["docker", "compose", "-f", cf, "config", "--format", "json"]
247
- )
243
+ result = _run(["docker", "compose", "-f", cf, "config", "--format", "json"])
248
244
  if result.returncode != 0:
249
245
  continue
250
246
  try:
@@ -282,9 +278,7 @@ def check_infra():
282
278
  services_depended_on: dict[str, str] = {} # depended_svc -> by_svc
283
279
 
284
280
  for label, cf in compose_files:
285
- result = _run(
286
- ["docker", "compose", "-f", cf, "config", "--format", "json"]
287
- )
281
+ result = _run(["docker", "compose", "-f", cf, "config", "--format", "json"])
288
282
  if result.returncode != 0:
289
283
  continue
290
284
  try:
@@ -321,9 +315,7 @@ def check_infra():
321
315
  click.echo(click.style(" Volumes", bold=True))
322
316
  all_volumes: dict[str, list[str]] = {} # vol_name -> [labels]
323
317
  for label, cf in compose_files:
324
- result = _run(
325
- ["docker", "compose", "-f", cf, "config", "--format", "json"]
326
- )
318
+ result = _run(["docker", "compose", "-f", cf, "config", "--format", "json"])
327
319
  if result.returncode != 0:
328
320
  continue
329
321
  try:
@@ -75,9 +75,7 @@ def db_migrate(feature, message, empty):
75
75
  and kept so you can write ``upgrade()``/``downgrade()`` by hand.
76
76
  """
77
77
  if empty and not feature:
78
- click.echo(
79
- click.style("❌ --empty requires a FEATURE argument.", fg="red")
80
- )
78
+ click.echo(click.style("❌ --empty requires a FEATURE argument.", fg="red"))
81
79
  raise SystemExit(1)
82
80
 
83
81
  if feature:
@@ -0,0 +1,253 @@
1
+ import glob
2
+ import importlib
3
+ import os
4
+ import tomllib
5
+
6
+ import click
7
+ from flask import current_app
8
+ from flask_migrate import upgrade as alembic_upgrade
9
+ from sqlalchemy import text, MetaData
10
+
11
+ from splent_cli.utils.decorators import requires_db
12
+ from splent_cli.services import context
13
+ from splent_cli.utils.lifecycle import advance_state, resolve_feature_key_from_entry
14
+ from splent_framework.db import db
15
+ from splent_framework.managers.migration_manager import (
16
+ MigrationManager,
17
+ SPLENT_MIGRATIONS_TABLE,
18
+ alembic_version_table,
19
+ )
20
+ from splent_framework.utils.feature_utils import get_features_from_pyproject
21
+ from splent_framework.utils.path_utils import PathUtils
22
+ from splent_cli.commands.clear.clear_uploads import clear_uploads
23
+
24
+
25
+ # =====================================================================
26
+ # Helpers for the per-feature reset
27
+ # =====================================================================
28
+ def _normalize(feature_name: str) -> str:
29
+ """Accept 'projects' or 'splent_feature_projects' -> full package name."""
30
+ short = feature_name.split("/")[-1].replace("splent_feature_", "")
31
+ return f"splent_feature_{short}"
32
+
33
+
34
+ def _tables_owned_by(feature_full: str) -> list[str]:
35
+ """Tables whose model classes live in the given feature's package."""
36
+ import_name = f"splent_io.{feature_full}"
37
+ try:
38
+ importlib.import_module(f"{import_name}.models")
39
+ except Exception:
40
+ pass
41
+ tables = []
42
+ for mapper in db.Model.registry.mappers:
43
+ cls = mapper.class_
44
+ if getattr(cls, "__module__", "").startswith(import_name):
45
+ tables.append(cls.__table__.name)
46
+ return sorted(set(tables))
47
+
48
+
49
+ def _refiners_of(feature_full: str, product_path: str) -> list[str]:
50
+ """Installed features whose [tool.splent.refinement].refines == feature."""
51
+ refiners = []
52
+ pattern = os.path.join(product_path, "features", "*", "*", "pyproject.toml")
53
+ for pp in glob.glob(pattern):
54
+ try:
55
+ with open(pp, "rb") as f:
56
+ data = tomllib.load(f)
57
+ except Exception:
58
+ continue
59
+ ref = data.get("tool", {}).get("splent", {}).get("refinement", {})
60
+ if ref.get("refines") == feature_full:
61
+ name = data.get("project", {}).get("name", "")
62
+ if name:
63
+ refiners.append(name)
64
+ return refiners
65
+
66
+
67
+ def _reapply(app, feats: list[str], entry_lookup: dict, product_path, product_name):
68
+ dirs = MigrationManager.get_all_feature_migration_dirs()
69
+ for feat in feats:
70
+ mdir = dirs.get(feat)
71
+ if not mdir:
72
+ continue
73
+ try:
74
+ alembic_upgrade(directory=mdir)
75
+ revision = MigrationManager.get_current_feature_revision(feat, db.engine)
76
+ MigrationManager.update_feature_status(app, feat, revision)
77
+ click.echo(click.style(f" ✅ {feat} → {revision or 'head'}", fg="green"))
78
+ info = entry_lookup.get(feat)
79
+ if info:
80
+ key, ns, name, version = info
81
+ advance_state(
82
+ product_path,
83
+ product_name,
84
+ key,
85
+ to="migrated",
86
+ namespace=ns,
87
+ name=name,
88
+ version=version,
89
+ )
90
+ except ImportError as e:
91
+ if "models" in str(e):
92
+ continue
93
+ click.echo(click.style(f" ❌ {feat}: {e}", fg="red"))
94
+ except Exception as e:
95
+ click.echo(click.style(f" ❌ {feat}: {e}", fg="red"))
96
+
97
+
98
+ def _entry_lookup() -> dict:
99
+ lookup = {}
100
+ for entry in get_features_from_pyproject() or []:
101
+ key, ns, name, version = resolve_feature_key_from_entry(entry)
102
+ lookup[name] = (key, ns, name, version)
103
+ return lookup
104
+
105
+
106
+ # =====================================================================
107
+ # Command
108
+ # =====================================================================
109
+ @requires_db
110
+ @click.command(
111
+ "db:reset",
112
+ short_help="Drop all tables (or just one feature's) and re-apply migrations.",
113
+ )
114
+ @click.argument("feature_name", required=False)
115
+ @click.option("-y", "--yes", is_flag=True, help="Confirm without prompting.")
116
+ @context.requires_product
117
+ def db_reset(feature_name, yes):
118
+ """Reset the database.
119
+
120
+ With no argument: drops ALL tables and re-applies every feature migration.
121
+
122
+ With FEATURE_NAME (e.g. ``db:reset projects``): drops only the tables that
123
+ feature owns, clears their migration tracking, and re-applies that
124
+ feature's migrations plus any feature that refines it — leaving every other
125
+ feature's data untouched. Useful after changing a model/refinement.
126
+ """
127
+ app = current_app
128
+ product_path = PathUtils.get_app_base_dir()
129
+ product_name = os.getenv("SPLENT_APP", "")
130
+
131
+ if feature_name:
132
+ _reset_one_feature(app, feature_name, yes, product_path, product_name)
133
+ return
134
+
135
+ _reset_everything(app, yes, product_path, product_name)
136
+
137
+
138
+ def _reset_one_feature(app, feature_name, yes, product_path, product_name):
139
+ feature_full = _normalize(feature_name)
140
+ tables = _tables_owned_by(feature_full)
141
+ refiners = _refiners_of(feature_full, product_path)
142
+ feats = [feature_full] + refiners
143
+
144
+ if not tables:
145
+ click.secho(
146
+ f"⚠️ '{feature_full}' owns no tables (nothing to drop). "
147
+ f"If it is a refinement, reset the base feature instead.",
148
+ fg="yellow",
149
+ )
150
+ return
151
+
152
+ click.echo()
153
+ click.secho(f"Reset feature: {feature_full}", fg="cyan", bold=True)
154
+ click.echo(f" tables to drop : {', '.join(tables)}")
155
+ if refiners:
156
+ click.echo(f" also re-applies: {', '.join(refiners)} (refiners)")
157
+ if not yes and not click.confirm(
158
+ "⚠️ This drops those tables and re-applies their migrations. Continue?",
159
+ abort=True,
160
+ ):
161
+ return
162
+
163
+ # Drop the feature's tables
164
+ with db.engine.begin() as conn:
165
+ conn.execute(text("SET FOREIGN_KEY_CHECKS = 0"))
166
+ for t in tables:
167
+ conn.execute(text(f"DROP TABLE IF EXISTS `{t}`"))
168
+ click.echo(click.style(f" Dropped {t}", fg="bright_black"))
169
+ conn.execute(text("SET FOREIGN_KEY_CHECKS = 1"))
170
+
171
+ # Clear migration tracking for the feature and its refiners — both the
172
+ # central splent_migrations row AND each feature's alembic version table —
173
+ # so their migrations are re-applied from scratch onto the fresh tables.
174
+ with db.engine.begin() as conn:
175
+ conn.execute(text("SET FOREIGN_KEY_CHECKS = 0"))
176
+ for feat in feats:
177
+ conn.execute(
178
+ text(f"DELETE FROM `{SPLENT_MIGRATIONS_TABLE}` WHERE feature = :f"),
179
+ {"f": feat},
180
+ )
181
+ conn.execute(text(f"DROP TABLE IF EXISTS `{alembic_version_table(feat)}`"))
182
+ conn.execute(text("SET FOREIGN_KEY_CHECKS = 1"))
183
+
184
+ click.secho("⬆️ Re-applying migrations...", fg="cyan")
185
+ _reapply(app, feats, _entry_lookup(), product_path, product_name)
186
+ click.secho(f"\n🎉 Feature '{feature_full}' reset complete.", fg="green")
187
+
188
+
189
+ def _reset_everything(app, yes, product_path, product_name):
190
+ if not yes and not click.confirm(
191
+ "⚠️ WARNING: This will DROP all tables and clear uploads. Are you sure?",
192
+ abort=True,
193
+ ):
194
+ return
195
+
196
+ click.echo(click.style("🗑️ Dropping all tables...", fg="yellow"))
197
+ try:
198
+ with db.engine.begin() as conn:
199
+ conn.execute(text("SET FOREIGN_KEY_CHECKS = 0"))
200
+ meta = MetaData()
201
+ meta.reflect(bind=db.engine)
202
+ for table in meta.sorted_tables:
203
+ conn.execute(text(f"DROP TABLE IF EXISTS `{table.name}`"))
204
+ click.echo(click.style(f" Dropped {table.name}", fg="bright_black"))
205
+ conn.execute(text("SET FOREIGN_KEY_CHECKS = 1"))
206
+ click.echo(click.style("✅ All tables dropped.", fg="yellow"))
207
+ except Exception as e:
208
+ click.echo(click.style(f"❌ Error dropping tables: {e}", fg="red"))
209
+ click.echo(
210
+ click.style(
211
+ "⚠️ The database may now be in a PARTIAL state: some tables "
212
+ "may have been dropped while others remain.\n"
213
+ " To recover, re-run this command once the database is "
214
+ "reachable again (it is safe to repeat — drops use "
215
+ "DROP TABLE IF EXISTS), or manually drop the remaining "
216
+ "tables / re-create the schema from migrations.",
217
+ fg="red",
218
+ )
219
+ )
220
+ raise SystemExit(1)
221
+
222
+ click.echo(click.style("📋 Recreating splent_migrations table...", fg="cyan"))
223
+ with db.engine.begin() as conn:
224
+ conn.execute(
225
+ text(
226
+ f"""
227
+ CREATE TABLE IF NOT EXISTS `{SPLENT_MIGRATIONS_TABLE}` (
228
+ `feature` VARCHAR(255) NOT NULL,
229
+ `last_migration` VARCHAR(255) DEFAULT NULL,
230
+ PRIMARY KEY (`feature`)
231
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
232
+ """
233
+ )
234
+ )
235
+
236
+ ctx = click.get_current_context()
237
+ ctx.invoke(clear_uploads)
238
+
239
+ dirs = MigrationManager.get_all_feature_migration_dirs()
240
+ if not dirs:
241
+ click.echo(click.style("⚠️ No feature migrations found.", fg="yellow"))
242
+ else:
243
+ click.echo(
244
+ click.style(
245
+ f"⬆️ Applying migrations for {len(dirs)} features...", fg="cyan"
246
+ )
247
+ )
248
+ _reapply(app, list(dirs.keys()), _entry_lookup(), product_path, product_name)
249
+
250
+ click.echo(click.style("\n🎉 Database reset complete.", fg="green"))
251
+
252
+
253
+ cli_command = db_reset
@@ -179,9 +179,7 @@ def db_seed(reset, yes, module):
179
179
  break
180
180
  except Exception as e:
181
181
  click.echo(
182
- click.style(
183
- f"❌ Error in {seeder.__class__.__name__}: {e}", fg="red"
184
- )
182
+ click.style(f"❌ Error in {seeder.__class__.__name__}: {e}", fg="red")
185
183
  )
186
184
  success = False
187
185
  break
@@ -68,7 +68,9 @@ def _check_workspace_precondition():
68
68
  wd = os.getenv("WORKING_DIR")
69
69
  workspace = wd or os.getcwd()
70
70
  if not wd:
71
- problems.append("WORKING_DIR is not set (falling back to the current directory)")
71
+ problems.append(
72
+ "WORKING_DIR is not set (falling back to the current directory)"
73
+ )
72
74
  elif not os.path.isdir(wd):
73
75
  problems.append(f"WORKING_DIR points to a missing directory: {wd}")
74
76
 
@@ -6,7 +6,7 @@ import click
6
6
  from splent_cli.services import context
7
7
  from splent_cli.utils.cache_utils import make_feature_readonly
8
8
  from splent_cli.utils.feature_utils import normalize_namespace
9
- from splent_cli.utils.proc import run, require_tool
9
+ from splent_cli.utils.proc import require_tool
10
10
 
11
11
 
12
12
  DEFAULT_NAMESPACE = os.getenv("SPLENT_DEFAULT_NAMESPACE", "splent_io")
@@ -20,8 +20,15 @@ DEFAULT_NAMESPACE = os.getenv("SPLENT_DEFAULT_NAMESPACE", "splent_io")
20
20
  def _get_latest_tag(namespace, repo) -> str | None:
21
21
  """Fetch the highest semver tag from GitHub. Returns None if unreachable or no tags."""
22
22
  api_url = f"https://api.github.com/repos/{namespace}/{repo}/tags?per_page=100"
23
+ # Authenticate when a token is present so version resolution also works for
24
+ # PRIVATE feature repos (an unauthenticated call 404s on those) and to raise
25
+ # the API rate limit.
26
+ headers = {}
27
+ token = os.getenv("GITHUB_TOKEN")
28
+ if token:
29
+ headers["Authorization"] = f"Bearer {token}"
23
30
  try:
24
- r = requests.get(api_url, timeout=5)
31
+ r = requests.get(api_url, headers=headers, timeout=5)
25
32
  r.raise_for_status()
26
33
  tags = r.json()
27
34
  if not tags:
@@ -42,16 +49,6 @@ def _get_latest_tag(namespace, repo) -> str | None:
42
49
  return None
43
50
 
44
51
 
45
- def _build_repo_url(namespace, repo):
46
- """Build the Git URL, trying SSH first with HTTPS fallback.
47
-
48
- Returns a tuple (real_url, display_url) where display_url never contains a token.
49
- """
50
- from splent_cli.utils.git_url import build_git_url
51
-
52
- return build_git_url(namespace, repo)
53
-
54
-
55
52
  def _parse_full_name(full_name: str):
56
53
  """
57
54
  full_name = <namespace>/<repo>[@version]
@@ -115,9 +112,6 @@ def feature_clone(full_name):
115
112
  )
116
113
  raise SystemExit(1)
117
114
 
118
- # Build Git URL based on your ownership
119
- fork_url, display_url = _build_repo_url(namespace, repo)
120
-
121
115
  # Local destination
122
116
  namespace_safe = normalize_namespace(namespace)
123
117
  workspace = str(context.workspace())
@@ -137,98 +131,51 @@ def feature_clone(full_name):
137
131
  "Install Git from https://git-scm.com/downloads and make sure it is on your PATH.",
138
132
  )
139
133
 
140
- click.secho(f"⬇️ Cloning {display_url}@{version}", fg="cyan")
141
-
142
- # Try clone the specific tag/branch (suppress git noise). Capture output so
143
- # we can tell WHY it failed instead of blindly assuming "version not found".
144
- result = run(
145
- [
146
- "git",
147
- "-c",
148
- "advice.detachedHead=false",
149
- "clone",
150
- "--depth",
151
- "1",
152
- "--branch",
153
- version,
154
- "--quiet",
155
- fork_url,
156
- local_path,
157
- ],
158
- check=False,
159
- capture=True,
134
+ from splent_cli.utils.git_url import (
135
+ clone as git_clone,
136
+ CLONE_SUCCESS,
137
+ CLONE_REF_NOT_FOUND,
160
138
  )
161
139
 
162
- if result.returncode != 0:
163
- shutil.rmtree(local_path, ignore_errors=True)
164
- stderr = (result.stderr or result.stdout or "").strip()
165
- stderr_lc = stderr.lower()
166
-
167
- # A genuinely missing ref is the ONLY case where falling back to main is
168
- # correct. git reports it as e.g.:
169
- # "Remote branch <x> not found in upstream origin"
170
- # "Could not find remote branch <x> to clone"
171
- ref_not_found = (
172
- "remote branch" in stderr_lc and "not found" in stderr_lc
173
- ) or "could not find remote branch" in stderr_lc
174
-
175
- # Auth / network / repo-access failures must NOT silently install main.
176
- auth_or_network = any(
177
- token in stderr_lc
178
- for token in (
179
- "authentication failed",
180
- "permission denied",
181
- "could not read from remote repository",
182
- "repository not found",
183
- "could not resolve host",
184
- "connection timed out",
185
- "connection refused",
186
- "network is unreachable",
187
- "failed to connect",
188
- "ssl",
189
- "tls",
190
- )
140
+ click.secho(f"⬇️ Cloning {namespace}/{repo}@{version}", fg="cyan")
141
+
142
+ # Always try SSH first, then HTTPS (GITHUB_TOKEN if set, else anonymous). The
143
+ # transport is decided per repo from the real clone result (see
144
+ # git_url.clone), so a working SSH key that simply can't read THIS repo no
145
+ # longer blocks the HTTPS path.
146
+ outcome, used_url, stderr = git_clone(namespace, repo, local_path, ref=version)
147
+
148
+ # A genuinely missing tag/branch is the ONLY case where falling back to the
149
+ # default branch is correct (the repo itself was reachable).
150
+ if outcome == CLONE_REF_NOT_FOUND:
151
+ click.secho(
152
+ f"⚠️ Version '{version}' not found. Cloning default branch instead.",
153
+ fg="yellow",
191
154
  )
155
+ outcome, used_url, stderr = git_clone(namespace, repo, local_path, ref=None)
192
156
 
193
- if ref_not_found and not auth_or_network:
194
- click.secho(
195
- f"⚠️ Version '{version}' not found. Cloning main instead.", fg="yellow"
196
- )
197
- fallback = run(
198
- ["git", "clone", "--depth", "1", "--quiet", fork_url, local_path],
199
- check=False,
200
- capture=True,
201
- )
202
- if fallback.returncode != 0:
203
- shutil.rmtree(local_path, ignore_errors=True)
204
- detail = (fallback.stderr or fallback.stdout or "").strip()
205
- click.secho(
206
- f"❌ Repository '{namespace}/{repo}' not found or not accessible.",
207
- fg="red",
208
- )
209
- if detail:
210
- click.secho(detail, fg="red")
211
- raise SystemExit(1)
212
- else:
213
- # Network / auth / unknown git failure: do NOT fall back to main,
214
- # which would silently install the wrong (unpinned) code.
215
- click.secho(
216
- f"❌ Failed to clone '{namespace}/{repo}@{version}'.", fg="red"
217
- )
218
- click.secho(
219
- "This looks like a network, authentication, or repository-access "
220
- "problem rather than a missing version. Check your connection and "
221
- "credentials, then try again.",
222
- fg="red",
223
- )
224
- if stderr:
225
- click.secho(stderr, fg="red")
226
- raise SystemExit(1)
157
+ if outcome != CLONE_SUCCESS:
158
+ shutil.rmtree(local_path, ignore_errors=True)
159
+ click.secho(
160
+ f"❌ Repository '{namespace}/{repo}' not found or not accessible "
161
+ "(tried SSH and HTTPS).",
162
+ fg="red",
163
+ )
164
+ click.secho(
165
+ "Check your network, that the repo exists, and that you have read "
166
+ "access an SSH key authorised for the repo, or a GITHUB_TOKEN with "
167
+ "read scope in your .env.",
168
+ fg="red",
169
+ )
170
+ if stderr:
171
+ click.secho(stderr, fg="red")
172
+ raise SystemExit(1)
227
173
 
228
174
  # Lock files as read-only to prevent accidental edits on pinned features
229
175
  make_feature_readonly(local_path)
230
176
 
231
177
  click.secho(
232
- f"✅ Feature '{namespace}/{repo}@{version}' cloned successfully.", fg="green"
178
+ f"✅ Feature '{namespace}/{repo}@{version}' cloned successfully ({used_url}).",
179
+ fg="green",
233
180
  )
234
181
  click.secho(f"🔒 Cached (read-only) at: {local_path}", fg="blue")
@@ -157,9 +157,7 @@ def _compile_in_container(container_id, feature, watch, production, workspace, p
157
157
  raise click.ClickException(
158
158
  f"'{run_cmd[0]}' is not installed or not on PATH.\n{tool_hint}"
159
159
  )
160
- click.echo(
161
- click.style(f"👀 Watching {feature} in {mode} mode...", fg="blue")
162
- )
160
+ click.echo(click.style(f"👀 Watching {feature} in {mode} mode...", fg="blue"))
163
161
  else:
164
162
  result = run(run_cmd, check=False, tool_hint=tool_hint)
165
163
  if result.returncode != 0:
@@ -121,9 +121,7 @@ def feature_env(feature_name, generate, env_name):
121
121
  try:
122
122
  shutil.copy(selected, env_file)
123
123
  except OSError as exc:
124
- click.echo(
125
- f"❌ Failed to create {env_file} from {selected}: {exc}"
126
- )
124
+ click.echo(f"❌ Failed to create {env_file} from {selected}: {exc}")
127
125
  raise SystemExit(1)
128
126
  click.echo(
129
127
  f"📄 Created {feature_entry}/docker/.env from {os.path.basename(selected)}"
@@ -241,7 +241,8 @@ def feature_install(feature_identifier, env_scope, mode, version):
241
241
  )
242
242
  if result.returncode != 0:
243
243
  click.secho(
244
- f" Clone failed: {(result.stderr or '').strip()}", fg="red"
244
+ f" Clone failed: {(result.stderr or '').strip()}",
245
+ fg="red",
245
246
  )
246
247
  raise SystemExit(1)
247
248
  else:
@@ -291,9 +292,7 @@ def feature_install(feature_identifier, env_scope, mode, version):
291
292
  check=False,
292
293
  )
293
294
  if dep.returncode != 0:
294
- click.secho(
295
- f" Installing dependency {m} failed.", fg="red"
296
- )
295
+ click.secho(f" Installing dependency {m} failed.", fg="red")
297
296
  raise SystemExit(1)
298
297
 
299
298
  # Attach to product
@@ -315,32 +314,27 @@ def feature_install(feature_identifier, env_scope, mode, version):
315
314
  # Clone to workspace root as editable
316
315
  click.echo(click.style(" clone ", dim=True) + f"{short} (editable)")
317
316
  from splent_cli.commands.feature.feature_clone import (
318
- _build_repo_url,
319
317
  _get_latest_tag,
320
318
  )
319
+ from splent_cli.utils.git_url import (
320
+ clone as git_clone,
321
+ CLONE_SUCCESS,
322
+ CLONE_REF_NOT_FOUND,
323
+ )
321
324
 
325
+ # SSH first, then HTTPS (per repo). ref=tag (None → default branch).
322
326
  tag = _get_latest_tag(namespace_github, feature_name)
323
- repo_url, _ = _build_repo_url(namespace_github, feature_name)
324
- branch = tag or "main"
325
- clone = run(
326
- [
327
- "git",
328
- "clone",
329
- "--depth",
330
- "1",
331
- "--branch",
332
- branch,
333
- "--quiet",
334
- repo_url,
335
- feature_dir,
336
- ],
337
- check=False,
338
- capture=True,
327
+ outcome, _used, err = git_clone(
328
+ namespace_github, feature_name, feature_dir, ref=tag
339
329
  )
340
- if clone.returncode != 0:
330
+ if outcome == CLONE_REF_NOT_FOUND:
331
+ outcome, _used, err = git_clone(
332
+ namespace_github, feature_name, feature_dir, ref=None
333
+ )
334
+ if outcome != CLONE_SUCCESS:
341
335
  click.secho(
342
- f" Could not clone {namespace_github}/{feature_name}: "
343
- f"{(clone.stderr or '').strip()}",
336
+ f" Could not clone {namespace_github}/{feature_name} "
337
+ f"(tried SSH and HTTPS): {err}",
344
338
  fg="red",
345
339
  )
346
340
  raise SystemExit(1)
@@ -386,9 +380,7 @@ def feature_install(feature_identifier, env_scope, mode, version):
386
380
  check=False,
387
381
  )
388
382
  if dep.returncode != 0:
389
- click.secho(
390
- f" Installing dependency {m} failed.", fg="red"
391
- )
383
+ click.secho(f" Installing dependency {m} failed.", fg="red")
392
384
  raise SystemExit(1)
393
385
 
394
386
  # Add to product
@@ -454,7 +446,8 @@ def feature_install(feature_identifier, env_scope, mode, version):
454
446
  )
455
447
  if result.returncode != 0:
456
448
  click.secho(
457
- f" Docker failed: {(result.stderr or '').strip()}", fg="yellow"
449
+ f" Docker failed: {(result.stderr or '').strip()}",
450
+ fg="yellow",
458
451
  )
459
452
 
460
453
  click.echo()