splent-cli 1.1.1__tar.gz → 1.2.6__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 (169) hide show
  1. {splent_cli-1.1.1/src/splent_cli.egg-info → splent_cli-1.2.6}/PKG-INFO +38 -2
  2. splent_cli-1.2.6/README.md +36 -0
  3. {splent_cli-1.1.1 → splent_cli-1.2.6}/pyproject.toml +6 -1
  4. {splent_cli-1.1.1 → splent_cli-1.2.6}/src/splent_cli/cli.py +7 -4
  5. splent_cli-1.2.6/src/splent_cli/commands/cache/cache_clear.py +85 -0
  6. splent_cli-1.2.6/src/splent_cli/commands/cache/cache_orphans.py +97 -0
  7. splent_cli-1.2.6/src/splent_cli/commands/cache/cache_outdated.py +127 -0
  8. splent_cli-1.2.6/src/splent_cli/commands/cache/cache_prune.py +123 -0
  9. splent_cli-1.2.6/src/splent_cli/commands/cache/cache_size.py +57 -0
  10. splent_cli-1.2.6/src/splent_cli/commands/cache/cache_status.py +86 -0
  11. splent_cli-1.2.6/src/splent_cli/commands/cache/cache_usage.py +59 -0
  12. splent_cli-1.2.6/src/splent_cli/commands/cache/cache_versions.py +46 -0
  13. splent_cli-1.2.6/src/splent_cli/commands/check/check_deps.py +257 -0
  14. splent_cli-1.2.6/src/splent_cli/commands/check/check_docker.py +96 -0
  15. splent_cli-1.2.6/src/splent_cli/commands/check/check_env.py +100 -0
  16. splent_cli-1.2.6/src/splent_cli/commands/check/check_features.py +147 -0
  17. {splent_cli-1.1.1 → splent_cli-1.2.6}/src/splent_cli/commands/check/check_github.py +25 -4
  18. {splent_cli-1.1.1 → splent_cli-1.2.6}/src/splent_cli/commands/check/check_pypi.py +43 -10
  19. splent_cli-1.2.6/src/splent_cli/commands/check/check_pyproject.py +114 -0
  20. {splent_cli-1.1.1 → splent_cli-1.2.6}/src/splent_cli/commands/clear_cache.py +5 -6
  21. splent_cli-1.2.6/src/splent_cli/commands/database/db_console.py +48 -0
  22. {splent_cli-1.1.1 → splent_cli-1.2.6}/src/splent_cli/commands/database/db_dump.py +15 -6
  23. splent_cli-1.2.6/src/splent_cli/commands/database/db_migrate.py +149 -0
  24. splent_cli-1.2.6/src/splent_cli/commands/database/db_reset.py +121 -0
  25. splent_cli-1.2.6/src/splent_cli/commands/database/db_restore.py +72 -0
  26. splent_cli-1.2.6/src/splent_cli/commands/database/db_rollback.py +60 -0
  27. splent_cli-1.2.6/src/splent_cli/commands/database/db_seed.py +170 -0
  28. splent_cli-1.2.6/src/splent_cli/commands/database/db_status.py +109 -0
  29. splent_cli-1.2.6/src/splent_cli/commands/database/db_upgrade.py +80 -0
  30. splent_cli-1.2.6/src/splent_cli/commands/doctor.py +126 -0
  31. splent_cli-1.2.6/src/splent_cli/commands/env/env_list.py +139 -0
  32. {splent_cli-1.1.1 → splent_cli-1.2.6}/src/splent_cli/commands/env/env_set.py +25 -15
  33. {splent_cli-1.1.1 → splent_cli-1.2.6}/src/splent_cli/commands/env/env_show.py +6 -7
  34. splent_cli-1.2.6/src/splent_cli/commands/export_puml.py +769 -0
  35. {splent_cli-1.1.1 → splent_cli-1.2.6}/src/splent_cli/commands/feature/feature_add.py +26 -16
  36. {splent_cli-1.1.1 → splent_cli-1.2.6}/src/splent_cli/commands/feature/feature_attach.py +48 -34
  37. {splent_cli-1.1.1 → splent_cli-1.2.6}/src/splent_cli/commands/feature/feature_clone.py +39 -18
  38. splent_cli-1.2.6/src/splent_cli/commands/feature/feature_contract.py +239 -0
  39. {splent_cli-1.1.1 → splent_cli-1.2.6}/src/splent_cli/commands/feature/feature_create.py +37 -29
  40. {splent_cli-1.1.1 → splent_cli-1.2.6}/src/splent_cli/commands/feature/feature_delete.py +11 -20
  41. splent_cli-1.2.6/src/splent_cli/commands/feature/feature_detach.py +101 -0
  42. splent_cli-1.2.6/src/splent_cli/commands/feature/feature_diff.py +523 -0
  43. {splent_cli-1.1.1 → splent_cli-1.2.6}/src/splent_cli/commands/feature/feature_discard.py +10 -8
  44. splent_cli-1.2.6/src/splent_cli/commands/feature/feature_drift.py +147 -0
  45. splent_cli-1.2.6/src/splent_cli/commands/feature/feature_edit.py +262 -0
  46. {splent_cli-1.1.1 → splent_cli-1.2.6}/src/splent_cli/commands/feature/feature_env.py +16 -11
  47. {splent_cli-1.1.1 → splent_cli-1.2.6}/src/splent_cli/commands/feature/feature_fork.py +14 -1
  48. splent_cli-1.2.6/src/splent_cli/commands/feature/feature_git.py +78 -0
  49. splent_cli-1.2.6/src/splent_cli/commands/feature/feature_hook_add.py +197 -0
  50. splent_cli-1.2.6/src/splent_cli/commands/feature/feature_hook_remove.py +199 -0
  51. splent_cli-1.2.6/src/splent_cli/commands/feature/feature_hooks.py +141 -0
  52. splent_cli-1.2.6/src/splent_cli/commands/feature/feature_list.py +51 -0
  53. splent_cli-1.2.6/src/splent_cli/commands/feature/feature_order.py +159 -0
  54. splent_cli-1.2.6/src/splent_cli/commands/feature/feature_pull.py +102 -0
  55. splent_cli-1.2.6/src/splent_cli/commands/feature/feature_release.py +447 -0
  56. splent_cli-1.2.6/src/splent_cli/commands/feature/feature_remove.py +115 -0
  57. {splent_cli-1.1.1 → splent_cli-1.2.6}/src/splent_cli/commands/feature/feature_rename.py +10 -10
  58. splent_cli-1.2.6/src/splent_cli/commands/feature/feature_search.py +126 -0
  59. splent_cli-1.2.6/src/splent_cli/commands/feature/feature_status.py +188 -0
  60. splent_cli-1.2.6/src/splent_cli/commands/feature/feature_sync_template.py +96 -0
  61. splent_cli-1.2.6/src/splent_cli/commands/feature/feature_test.py +269 -0
  62. splent_cli-1.2.6/src/splent_cli/commands/feature/feature_upgrade.py +232 -0
  63. splent_cli-1.2.6/src/splent_cli/commands/feature/feature_versions.py +410 -0
  64. splent_cli-1.2.6/src/splent_cli/commands/feature_compile.py +140 -0
  65. splent_cli-1.2.6/src/splent_cli/commands/linter.py +135 -0
  66. {splent_cli-1.1.1 → splent_cli-1.2.6}/src/splent_cli/commands/product/product_build.py +3 -7
  67. splent_cli-1.2.6/src/splent_cli/commands/product/product_clean.py +113 -0
  68. {splent_cli-1.1.1 → splent_cli-1.2.6}/src/splent_cli/commands/product/product_create.py +16 -4
  69. {splent_cli-1.1.1 → splent_cli-1.2.6}/src/splent_cli/commands/product/product_deploy.py +12 -11
  70. splent_cli-1.2.6/src/splent_cli/commands/product/product_derive.py +136 -0
  71. {splent_cli-1.1.1 → splent_cli-1.2.6}/src/splent_cli/commands/product/product_down.py +16 -42
  72. splent_cli-1.2.6/src/splent_cli/commands/product/product_drift.py +139 -0
  73. {splent_cli-1.1.1 → splent_cli-1.2.6}/src/splent_cli/commands/product/product_env.py +21 -28
  74. splent_cli-1.2.6/src/splent_cli/commands/product/product_list.py +58 -0
  75. splent_cli-1.2.6/src/splent_cli/commands/product/product_logs.py +64 -0
  76. {splent_cli-1.1.1 → splent_cli-1.2.6}/src/splent_cli/commands/product/product_port.py +22 -17
  77. splent_cli-1.2.6/src/splent_cli/commands/product/product_release.py +113 -0
  78. {splent_cli-1.1.1 → splent_cli-1.2.6}/src/splent_cli/commands/product/product_run.py +8 -32
  79. {splent_cli-1.1.1 → splent_cli-1.2.6}/src/splent_cli/commands/product/product_select.py +6 -4
  80. splent_cli-1.2.6/src/splent_cli/commands/product/product_shell.py +89 -0
  81. splent_cli-1.2.6/src/splent_cli/commands/product/product_status.py +131 -0
  82. splent_cli-1.2.6/src/splent_cli/commands/product/product_sync.py +143 -0
  83. splent_cli-1.2.6/src/splent_cli/commands/product/product_sync_template.py +132 -0
  84. splent_cli-1.2.6/src/splent_cli/commands/product/product_up.py +66 -0
  85. {splent_cli-1.1.1 → splent_cli-1.2.6}/src/splent_cli/commands/release/release_core.py +79 -23
  86. {splent_cli-1.1.1 → splent_cli-1.2.6}/src/splent_cli/commands/route_list.py +2 -2
  87. {splent_cli-1.1.1 → splent_cli-1.2.6}/src/splent_cli/commands/selenium.py +2 -1
  88. splent_cli-1.2.6/src/splent_cli/commands/tokens.py +71 -0
  89. splent_cli-1.2.6/src/splent_cli/commands/uvl/uvl_check.py +150 -0
  90. splent_cli-1.2.6/src/splent_cli/commands/uvl/uvl_configs.py +58 -0
  91. splent_cli-1.2.6/src/splent_cli/commands/uvl/uvl_deps.py +93 -0
  92. splent_cli-1.2.6/src/splent_cli/commands/uvl/uvl_features.py +49 -0
  93. splent_cli-1.2.6/src/splent_cli/commands/uvl/uvl_fetch.py +56 -0
  94. splent_cli-1.2.6/src/splent_cli/commands/uvl/uvl_fix.py +278 -0
  95. splent_cli-1.2.6/src/splent_cli/commands/uvl/uvl_info.py +61 -0
  96. splent_cli-1.2.6/src/splent_cli/commands/uvl/uvl_missing.py +92 -0
  97. {splent_cli-1.1.1 → splent_cli-1.2.6}/src/splent_cli/commands/uvl/uvl_sync.py +54 -170
  98. splent_cli-1.2.6/src/splent_cli/commands/uvl/uvl_utils.py +183 -0
  99. splent_cli-1.2.6/src/splent_cli/commands/uvl/uvl_valid.py +81 -0
  100. splent_cli-1.2.6/src/splent_cli/commands/version.py +208 -0
  101. splent_cli-1.2.6/src/splent_cli/services/__init__.py +0 -0
  102. splent_cli-1.2.6/src/splent_cli/services/compose.py +137 -0
  103. splent_cli-1.2.6/src/splent_cli/services/context.py +23 -0
  104. splent_cli-1.2.6/src/splent_cli/services/release.py +158 -0
  105. splent_cli-1.2.6/src/splent_cli/utils/__init__.py +0 -0
  106. splent_cli-1.2.6/src/splent_cli/utils/cache_utils.py +54 -0
  107. {splent_cli-1.1.1 → splent_cli-1.2.6}/src/splent_cli/utils/command_loader.py +5 -4
  108. splent_cli-1.2.6/src/splent_cli/utils/db_utils.py +28 -0
  109. splent_cli-1.2.6/src/splent_cli/utils/decorators.py +17 -0
  110. {splent_cli-1.1.1 → splent_cli-1.2.6}/src/splent_cli/utils/dynamic_imports.py +1 -1
  111. {splent_cli-1.1.1 → splent_cli-1.2.6}/src/splent_cli/utils/feature_installer.py +5 -3
  112. splent_cli-1.2.6/src/splent_cli/utils/feature_utils.py +71 -0
  113. splent_cli-1.2.6/src/splent_cli/utils/lifecycle.py +238 -0
  114. splent_cli-1.2.6/src/splent_cli/utils/manifest.py +216 -0
  115. splent_cli-1.2.6/src/splent_cli/utils/path_utils.py +37 -0
  116. splent_cli-1.2.6/src/splent_cli/utils/template_drift.py +173 -0
  117. {splent_cli-1.1.1 → splent_cli-1.2.6/src/splent_cli.egg-info}/PKG-INFO +38 -2
  118. {splent_cli-1.1.1 → splent_cli-1.2.6}/src/splent_cli.egg-info/SOURCES.txt +54 -4
  119. {splent_cli-1.1.1 → splent_cli-1.2.6}/src/splent_cli.egg-info/requires.txt +1 -0
  120. splent_cli-1.1.1/README.md +0 -1
  121. splent_cli-1.1.1/src/splent_cli/commands/clear_features.py +0 -64
  122. splent_cli-1.1.1/src/splent_cli/commands/database/db_console.py +0 -23
  123. splent_cli-1.1.1/src/splent_cli/commands/database/db_migrate.py +0 -48
  124. splent_cli-1.1.1/src/splent_cli/commands/database/db_reset.py +0 -107
  125. splent_cli-1.1.1/src/splent_cli/commands/database/db_rollback.py +0 -34
  126. splent_cli-1.1.1/src/splent_cli/commands/database/db_seed.py +0 -103
  127. splent_cli-1.1.1/src/splent_cli/commands/database/db_status.py +0 -40
  128. splent_cli-1.1.1/src/splent_cli/commands/database/db_upgrade.py +0 -42
  129. splent_cli-1.1.1/src/splent_cli/commands/doctor.py +0 -281
  130. splent_cli-1.1.1/src/splent_cli/commands/feature/feature_detach.py +0 -74
  131. splent_cli-1.1.1/src/splent_cli/commands/feature/feature_edit.py +0 -193
  132. splent_cli-1.1.1/src/splent_cli/commands/feature/feature_list.py +0 -31
  133. splent_cli-1.1.1/src/splent_cli/commands/feature/feature_release.py +0 -316
  134. splent_cli-1.1.1/src/splent_cli/commands/feature/feature_remove.py +0 -72
  135. splent_cli-1.1.1/src/splent_cli/commands/linter.py +0 -43
  136. splent_cli-1.1.1/src/splent_cli/commands/product/product_release.py +0 -249
  137. splent_cli-1.1.1/src/splent_cli/commands/product/product_sync.py +0 -105
  138. splent_cli-1.1.1/src/splent_cli/commands/product/product_up.py +0 -93
  139. splent_cli-1.1.1/src/splent_cli/commands/test.py +0 -122
  140. splent_cli-1.1.1/src/splent_cli/commands/uvl/uvl_check.py +0 -234
  141. splent_cli-1.1.1/src/splent_cli/commands/uvl/uvl_configs.py +0 -86
  142. splent_cli-1.1.1/src/splent_cli/commands/uvl/uvl_deps.py +0 -130
  143. splent_cli-1.1.1/src/splent_cli/commands/uvl/uvl_features.py +0 -124
  144. splent_cli-1.1.1/src/splent_cli/commands/uvl/uvl_fetch.py +0 -99
  145. splent_cli-1.1.1/src/splent_cli/commands/uvl/uvl_info.py +0 -98
  146. splent_cli-1.1.1/src/splent_cli/commands/uvl/uvl_missing.py +0 -211
  147. splent_cli-1.1.1/src/splent_cli/commands/uvl/uvl_valid.py +0 -164
  148. splent_cli-1.1.1/src/splent_cli/commands/version.py +0 -60
  149. splent_cli-1.1.1/src/splent_cli/commands/webpack_compile.py +0 -118
  150. splent_cli-1.1.1/src/splent_cli/utils/decorators.py +0 -6
  151. splent_cli-1.1.1/src/splent_cli/utils/feature_utils.py +0 -39
  152. splent_cli-1.1.1/src/splent_cli/utils/path_utils.py +0 -90
  153. {splent_cli-1.1.1 → splent_cli-1.2.6}/LICENSE +0 -0
  154. {splent_cli-1.1.1 → splent_cli-1.2.6}/setup.cfg +0 -0
  155. {splent_cli-1.1.1 → splent_cli-1.2.6}/src/splent_cli/__init__.py +0 -0
  156. {splent_cli-1.1.1 → splent_cli-1.2.6}/src/splent_cli/__main__.py +0 -0
  157. {splent_cli-1.1.1 → splent_cli-1.2.6}/src/splent_cli/commands/__init__.py +0 -0
  158. {splent_cli-1.1.1/src/splent_cli/commands/check → splent_cli-1.2.6/src/splent_cli/commands/cache}/__init__.py +0 -0
  159. {splent_cli-1.1.1/src/splent_cli/commands/product → splent_cli-1.2.6/src/splent_cli/commands/check}/__init__.py +0 -0
  160. {splent_cli-1.1.1 → splent_cli-1.2.6}/src/splent_cli/commands/clear_log.py +0 -0
  161. {splent_cli-1.1.1 → splent_cli-1.2.6}/src/splent_cli/commands/clear_uploads.py +0 -0
  162. {splent_cli-1.1.1 → splent_cli-1.2.6}/src/splent_cli/commands/command_create.py +0 -0
  163. {splent_cli-1.1.1 → splent_cli-1.2.6}/src/splent_cli/commands/coverage.py +0 -0
  164. {splent_cli-1.1.1 → splent_cli-1.2.6}/src/splent_cli/commands/locust.py +0 -0
  165. {splent_cli-1.1.1/src/splent_cli/commands/release → splent_cli-1.2.6/src/splent_cli/commands/product}/__init__.py +0 -0
  166. {splent_cli-1.1.1/src/splent_cli/utils → splent_cli-1.2.6/src/splent_cli/commands/release}/__init__.py +0 -0
  167. {splent_cli-1.1.1 → splent_cli-1.2.6}/src/splent_cli.egg-info/dependency_links.txt +0 -0
  168. {splent_cli-1.1.1 → splent_cli-1.2.6}/src/splent_cli.egg-info/entry_points.txt +0 -0
  169. {splent_cli-1.1.1 → splent_cli-1.2.6}/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.1.1
3
+ Version: 1.2.6
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,6 +9,7 @@ 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: packaging>=24.0
12
13
  Provides-Extra: dev
13
14
  Requires-Dist: setuptools==80.3.1; extra == "dev"
14
15
  Requires-Dist: pytest==8.3.4; extra == "dev"
@@ -38,4 +39,39 @@ Requires-Dist: tomli_w==1.2.0; extra == "dev"
38
39
  Requires-Dist: PyYAML==6.0.3; extra == "dev"
39
40
  Dynamic: license-file
40
41
 
41
- # splent_cli
42
+ # SPLENT CLI
43
+
44
+ Command-line tool for managing SPLENT products, features, databases, and environments.
45
+
46
+ ## Quick start
47
+
48
+ ```bash
49
+ make setup # Prepare .env, start Docker, enter CLI container
50
+ splent --help # See all available commands
51
+ ```
52
+
53
+ ## Key commands
54
+
55
+ | Command | Description |
56
+ |---------|-------------|
57
+ | `product:create` | Create a new product |
58
+ | `product:derive --dev` | Full SPL derivation pipeline |
59
+ | `feature:add` / `feature:attach` | Add features to a product |
60
+ | `feature:status` | Show feature lifecycle states |
61
+ | `feature:release` | Release a feature (tag + PyPI + GitHub) |
62
+ | `db:migrate` / `db:upgrade` | Manage per-feature migrations |
63
+ | `export:puml` | Generate PlantUML diagrams |
64
+ | `doctor` | System health check |
65
+
66
+ ## Requirements
67
+
68
+ - Docker + Docker Compose
69
+ - Python 3.13+
70
+
71
+ ## Documentation
72
+
73
+ Full documentation at **[docs.splent.io](https://docs.splent.io)**
74
+
75
+ ## License
76
+
77
+ Creative Commons CC BY 4.0 - SPLENT - Diverso Lab
@@ -0,0 +1,36 @@
1
+ # SPLENT CLI
2
+
3
+ Command-line tool for managing SPLENT products, features, databases, and environments.
4
+
5
+ ## Quick start
6
+
7
+ ```bash
8
+ make setup # Prepare .env, start Docker, enter CLI container
9
+ splent --help # See all available commands
10
+ ```
11
+
12
+ ## Key commands
13
+
14
+ | Command | Description |
15
+ |---------|-------------|
16
+ | `product:create` | Create a new product |
17
+ | `product:derive --dev` | Full SPL derivation pipeline |
18
+ | `feature:add` / `feature:attach` | Add features to a product |
19
+ | `feature:status` | Show feature lifecycle states |
20
+ | `feature:release` | Release a feature (tag + PyPI + GitHub) |
21
+ | `db:migrate` / `db:upgrade` | Manage per-feature migrations |
22
+ | `export:puml` | Generate PlantUML diagrams |
23
+ | `doctor` | System health check |
24
+
25
+ ## Requirements
26
+
27
+ - Docker + Docker Compose
28
+ - Python 3.13+
29
+
30
+ ## Documentation
31
+
32
+ Full documentation at **[docs.splent.io](https://docs.splent.io)**
33
+
34
+ ## License
35
+
36
+ Creative Commons CC BY 4.0 - SPLENT - Diverso Lab
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "splent_cli"
7
- version = "1.1.1"
7
+ version = "1.2.6"
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"
@@ -13,6 +13,7 @@ license-files = ["LICENSE"]
13
13
 
14
14
  dependencies = [
15
15
  "click==8.1.8",
16
+ "packaging>=24.0",
16
17
  ]
17
18
 
18
19
  [project.optional-dependencies]
@@ -54,6 +55,10 @@ package-dir = { "" = "src" }
54
55
  [tool.setuptools.packages.find]
55
56
  where = ["src"]
56
57
 
58
+ [tool.pytest.ini_options]
59
+ testpaths = ["tests"]
60
+ pythonpath = ["src"]
61
+
57
62
  [tool.black]
58
63
  line-length = 79
59
64
 
@@ -3,6 +3,7 @@ from dotenv import load_dotenv
3
3
 
4
4
  from splent_cli.utils.dynamic_imports import get_app
5
5
  from splent_cli.utils.command_loader import load_commands
6
+ from splent_cli.utils.db_utils import check_db_connection
6
7
 
7
8
  load_dotenv()
8
9
 
@@ -12,6 +13,7 @@ class SPLENTCLI(click.Group):
12
13
  Main SPLENT CLI class.
13
14
 
14
15
  - Automatically injects the Flask app context for commands marked with `requires_app = True`.
16
+ - Checks DB connectivity for commands marked with `requires_db = True`.
15
17
  - Displays commands grouped by category for a cleaner, more readable help output.
16
18
  """
17
19
 
@@ -21,6 +23,9 @@ class SPLENTCLI(click.Group):
21
23
 
22
24
  if command and getattr(command, "requires_app", False):
23
25
  app = get_app()
26
+ if getattr(command, "requires_db", False):
27
+ if not check_db_connection(app):
28
+ raise SystemExit(1)
24
29
  with app.app_context():
25
30
  return super().invoke(ctx)
26
31
 
@@ -39,11 +44,12 @@ class SPLENTCLI(click.Group):
39
44
  cmd for cmd in self.commands if cmd.startswith("uvl:")
40
45
  ],
41
46
  "🧱 Database": [cmd for cmd in self.commands if cmd.startswith("db:")],
47
+ "💾 Cache": [cmd for cmd in self.commands if cmd.startswith("cache:")],
42
48
  "🧰 Utilities": [
43
49
  cmd
44
50
  for cmd in self.commands
45
51
  if cmd.startswith(
46
- ("clear:", "env", "select", "info", "version", "doctor")
52
+ ("clear:", "env", "select", "info", "version", "doctor", "tokens")
47
53
  )
48
54
  ],
49
55
  "🐍 Development & QA": [
@@ -51,9 +57,6 @@ class SPLENTCLI(click.Group):
51
57
  for cmd in self.commands
52
58
  if cmd.startswith(("linter", "test", "coverage", "locust"))
53
59
  ],
54
- "⚙️ Build & Assets": [
55
- cmd for cmd in self.commands if cmd.startswith("webpack:")
56
- ],
57
60
  }
58
61
  for title, cmds in groups.items():
59
62
  cmds = [c for c in cmds if c in self.commands]
@@ -0,0 +1,85 @@
1
+ from splent_cli.services import context, compose
2
+ import shutil
3
+ import click
4
+ from pathlib import Path
5
+
6
+
7
+ @click.command("cache:clear", short_help="Clear the feature cache (total or partial).")
8
+ @click.option(
9
+ "--namespace",
10
+ default=None,
11
+ help="Clear only a specific namespace (e.g. splent_io).",
12
+ )
13
+ @click.option(
14
+ "--feature",
15
+ default=None,
16
+ help="Clear all entries for a specific feature (e.g. splent_feature_auth).",
17
+ )
18
+ @click.option("--yes", is_flag=True, help="Skip confirmation prompt.")
19
+ def cache_clear(namespace, feature, yes):
20
+ """
21
+ Deletes entries from the feature cache and removes broken symlinks in products.
22
+
23
+ \b
24
+ Scope (most to least specific):
25
+ --feature splent_feature_auth → removes all cache entries for that feature
26
+ --namespace splent_io → removes the entire namespace folder
27
+ (no options) → clears the entire feature cache
28
+ """
29
+ workspace = context.workspace()
30
+ cache_root = workspace / ".splent_cache" / "features"
31
+
32
+ if not cache_root.exists():
33
+ click.secho("⚠️ No .splent_cache/features directory found.", fg="yellow")
34
+ raise SystemExit(0)
35
+
36
+ # Resolve targets
37
+ if feature:
38
+ targets = []
39
+ search_in = [cache_root / namespace] if namespace else cache_root.iterdir()
40
+ for ns_dir in search_in if namespace else cache_root.iterdir():
41
+ ns_dir = Path(ns_dir)
42
+ if not ns_dir.is_dir():
43
+ continue
44
+ for feat_dir in ns_dir.iterdir():
45
+ base = feat_dir.name.split("@")[0]
46
+ if base == feature:
47
+ targets.append(feat_dir)
48
+ if not targets:
49
+ label = f"'{feature}'" + (
50
+ f" in namespace '{namespace}'" if namespace else ""
51
+ )
52
+ click.secho(f"⚠️ No cache entries found for {label}.", fg="yellow")
53
+ raise SystemExit(0)
54
+ description = f"{len(targets)} entry/entries for feature '{feature}'"
55
+ elif namespace:
56
+ targets = [cache_root / namespace]
57
+ if not targets[0].exists():
58
+ click.secho(f"⚠️ Namespace '{namespace}' not found in cache.", fg="yellow")
59
+ raise SystemExit(0)
60
+ description = f"namespace '{namespace}'"
61
+ else:
62
+ targets = [cache_root]
63
+ description = "entire feature cache"
64
+
65
+ if not yes and not click.confirm(
66
+ f"⚠️ This will permanently delete the {description}. Continue?"
67
+ ):
68
+ click.echo("❎ Cancelled.")
69
+ raise SystemExit(0)
70
+
71
+ # Delete
72
+ for t in targets:
73
+ shutil.rmtree(t)
74
+ cache_root.mkdir(parents=True, exist_ok=True)
75
+ click.secho(f"🧹 Cleared: {description}.", fg="green")
76
+
77
+ # Clean broken symlinks
78
+ removed = compose.remove_broken_symlinks(workspace)
79
+ if removed:
80
+ click.secho(f"🔗 Removed {removed} broken feature symlink(s).", fg="yellow")
81
+ else:
82
+ click.secho("✅ No broken symlinks found.", fg="green")
83
+
84
+
85
+ cli_command = cache_clear
@@ -0,0 +1,97 @@
1
+ from splent_cli.services import context
2
+ import click
3
+ from pathlib import Path
4
+
5
+
6
+ def _get_cache_entries(cache_root: Path) -> list:
7
+ """Returns list of {namespace, name, version, is_versioned} dicts."""
8
+ entries = []
9
+ if not cache_root.exists():
10
+ return entries
11
+ for ns_dir in sorted(cache_root.iterdir()):
12
+ if not ns_dir.is_dir():
13
+ continue
14
+ for feat_dir in sorted(ns_dir.iterdir()):
15
+ if not feat_dir.is_dir():
16
+ continue
17
+ feat = feat_dir.name
18
+ if "@" in feat:
19
+ name, version = feat.split("@", 1)
20
+ entries.append(
21
+ {
22
+ "namespace": ns_dir.name,
23
+ "name": name,
24
+ "version": version,
25
+ "is_versioned": True,
26
+ }
27
+ )
28
+ else:
29
+ entries.append(
30
+ {
31
+ "namespace": ns_dir.name,
32
+ "name": feat,
33
+ "version": None,
34
+ "is_versioned": False,
35
+ }
36
+ )
37
+ return entries
38
+
39
+
40
+ def _get_all_product_refs(workspace: Path) -> set:
41
+ """Returns set of 'name' and 'name@version' (no namespace) from all products' pyproject.toml."""
42
+ import tomllib
43
+ refs = set()
44
+ for product_dir in sorted(workspace.iterdir()):
45
+ if not product_dir.is_dir() or product_dir.name.startswith("."):
46
+ continue
47
+ pyproject = product_dir / "pyproject.toml"
48
+ if not pyproject.exists():
49
+ continue
50
+ try:
51
+ with open(pyproject, "rb") as f:
52
+ data = tomllib.load(f)
53
+ splent = data.get("tool", {}).get("splent", {})
54
+ feats = list(splent.get("features", []))
55
+ feats += list(splent.get("features_dev", []))
56
+ feats += list(splent.get("features_prod", []))
57
+ for entry in feats:
58
+ ref = entry.split("/", 1)[1] if "/" in entry else entry
59
+ refs.add(ref)
60
+ except Exception:
61
+ continue
62
+ return refs
63
+
64
+
65
+ @click.command(
66
+ "cache:orphans", short_help="Show cached features not referenced by any product."
67
+ )
68
+ def cache_orphans():
69
+ """Lists features in the cache that no product references in its pyproject.toml."""
70
+ workspace = context.workspace()
71
+ cache_root = workspace / ".splent_cache" / "features"
72
+
73
+ entries = _get_cache_entries(cache_root)
74
+ if not entries:
75
+ click.secho("ℹ️ Feature cache is empty.", fg="yellow")
76
+ return
77
+
78
+ refs = _get_all_product_refs(workspace)
79
+
80
+ orphans = []
81
+ for e in entries:
82
+ # Match against just name[@version] — pyproject doesn't include namespace
83
+ ref_key = f"{e['name']}@{e['version']}" if e["is_versioned"] else e["name"]
84
+ if ref_key not in refs:
85
+ display_key = f"{e['namespace']}/{ref_key}"
86
+ orphans.append(display_key)
87
+
88
+ if not orphans:
89
+ click.secho("✅ No orphaned features in cache.", fg="green")
90
+ return
91
+
92
+ click.secho(f"🗑 Orphaned cache entries ({len(orphans)}):", fg="yellow")
93
+ for o in orphans:
94
+ click.echo(f" - {o}")
95
+
96
+
97
+ cli_command = cache_orphans
@@ -0,0 +1,127 @@
1
+ from splent_cli.services import context
2
+ import re
3
+ import click
4
+ from pathlib import Path
5
+ from collections import defaultdict
6
+ from packaging.version import Version, InvalidVersion
7
+
8
+
9
+ def _get_cache_versions(cache_root: Path) -> dict:
10
+ """Returns {name: [version, ...]} with all versioned snapshots in cache."""
11
+ versions = defaultdict(list)
12
+ if not cache_root.exists():
13
+ return versions
14
+ for ns_dir in cache_root.iterdir():
15
+ if not ns_dir.is_dir():
16
+ continue
17
+ for feat_dir in ns_dir.iterdir():
18
+ if not feat_dir.is_dir() or "@" not in feat_dir.name:
19
+ continue
20
+ name, version = feat_dir.name.split("@", 1)
21
+ versions[name].append(version)
22
+ return versions
23
+
24
+
25
+ def _latest(versions: list) -> str:
26
+ """Returns the latest version string, using semver if possible."""
27
+
28
+ def sort_key(v):
29
+ try:
30
+ return Version(v.lstrip("v"))
31
+ except InvalidVersion:
32
+ return Version("0")
33
+
34
+ return max(versions, key=sort_key)
35
+
36
+
37
+ def _get_product_features(workspace: Path) -> dict:
38
+ """Returns {product_name: {feature_name: version_or_None}}."""
39
+ 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
54
+ 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
66
+ return products
67
+
68
+
69
+ @click.command(
70
+ "cache:outdated",
71
+ short_help="Show products using older versions than what's in cache.",
72
+ )
73
+ def cache_outdated():
74
+ """
75
+ Compares the version each product uses against all versions available in cache.
76
+ Reports features where a newer version exists locally.
77
+ """
78
+ workspace = context.workspace()
79
+ cache_root = workspace / ".splent_cache" / "features"
80
+
81
+ cache_versions = _get_cache_versions(cache_root)
82
+ if not cache_versions:
83
+ click.secho("ℹ️ No versioned snapshots in cache.", fg="yellow")
84
+ return
85
+
86
+ products = _get_product_features(workspace)
87
+ if not products:
88
+ click.secho("ℹ️ No products with declared features found.", fg="yellow")
89
+ return
90
+
91
+ outdated = []
92
+ for product, features in sorted(products.items()):
93
+ for name, current_ver in features.items():
94
+ available = cache_versions.get(name)
95
+ if not available:
96
+ continue
97
+ if current_ver is None:
98
+ # Editable — show available versions as informational
99
+ outdated.append(
100
+ (product, name, "(editable)", _latest(available), available)
101
+ )
102
+ continue
103
+ latest = _latest(available)
104
+ try:
105
+ is_old = Version(latest.lstrip("v")) > Version(current_ver.lstrip("v"))
106
+ except InvalidVersion:
107
+ is_old = latest != current_ver
108
+ if is_old:
109
+ outdated.append((product, name, current_ver, latest, available))
110
+
111
+ if not outdated:
112
+ click.secho("✅ All products are on the latest cached version.", fg="green")
113
+ return
114
+
115
+ click.secho(f"Outdated features ({len(outdated)}):\n", fg="yellow")
116
+ for product, name, current, latest, available in outdated:
117
+ click.echo(
118
+ f" {click.style(product, bold=True)} {name} "
119
+ f"{click.style(current, fg='red')} → {click.style(latest, fg='green')}"
120
+ )
121
+ others = sorted(set(available) - {latest})
122
+ if others:
123
+ click.echo(f" also cached: {', '.join(others)}")
124
+ click.echo()
125
+
126
+
127
+ cli_command = cache_outdated
@@ -0,0 +1,123 @@
1
+ from splent_cli.services import context, compose
2
+ import re
3
+ import shutil
4
+ import click
5
+ from pathlib import Path
6
+
7
+
8
+ def _get_cache_entries(cache_root: Path) -> list:
9
+ entries = []
10
+ if not cache_root.exists():
11
+ return entries
12
+ for ns_dir in sorted(cache_root.iterdir()):
13
+ if not ns_dir.is_dir():
14
+ continue
15
+ for feat_dir in sorted(ns_dir.iterdir()):
16
+ if not feat_dir.is_dir():
17
+ continue
18
+ feat = feat_dir.name
19
+ if "@" in feat:
20
+ name, version = feat.split("@", 1)
21
+ entries.append(
22
+ {
23
+ "namespace": ns_dir.name,
24
+ "name": name,
25
+ "version": version,
26
+ "is_versioned": True,
27
+ "path": feat_dir,
28
+ }
29
+ )
30
+ else:
31
+ entries.append(
32
+ {
33
+ "namespace": ns_dir.name,
34
+ "name": feat,
35
+ "version": None,
36
+ "is_versioned": False,
37
+ "path": feat_dir,
38
+ }
39
+ )
40
+ return entries
41
+
42
+
43
+ def _get_all_product_refs(workspace: Path) -> set:
44
+ refs = set()
45
+ for product_dir in sorted(workspace.iterdir()):
46
+ if not product_dir.is_dir() or product_dir.name.startswith("."):
47
+ continue
48
+ pyproject = product_dir / "pyproject.toml"
49
+ if not pyproject.exists():
50
+ continue
51
+ content = pyproject.read_text()
52
+ m = re.search(
53
+ r"\[project\.optional-dependencies\].*?features\s*=\s*\[(.*?)\]",
54
+ content,
55
+ re.DOTALL,
56
+ )
57
+ if not m:
58
+ continue
59
+ for raw in re.findall(r'"([^"]+)"|\'([^\']+)\'', m.group(1)):
60
+ ref = raw[0] or raw[1]
61
+ if "/" in ref:
62
+ ref = ref.split("/", 1)[1]
63
+ refs.add(ref)
64
+ return refs
65
+
66
+
67
+ @click.command(
68
+ "cache:prune", short_help="Remove orphaned cache entries not used by any product."
69
+ )
70
+ @click.option("--yes", is_flag=True, help="Skip confirmation prompt.")
71
+ def cache_prune(yes):
72
+ """
73
+ Removes cache entries that no product references in its pyproject.toml,
74
+ then cleans up broken symlinks in products.
75
+ """
76
+ workspace = context.workspace()
77
+ cache_root = workspace / ".splent_cache" / "features"
78
+
79
+ entries = _get_cache_entries(cache_root)
80
+ if not entries:
81
+ click.secho("ℹ️ Feature cache is empty.", fg="yellow")
82
+ return
83
+
84
+ refs = _get_all_product_refs(workspace)
85
+
86
+ orphans = [
87
+ e
88
+ for e in entries
89
+ if (f"{e['name']}@{e['version']}" if e["is_versioned"] else e["name"])
90
+ not in refs
91
+ ]
92
+
93
+ if not orphans:
94
+ click.secho("✅ Nothing to prune — no orphaned entries.", fg="green")
95
+ return
96
+
97
+ click.secho(f"Orphaned entries to remove ({len(orphans)}):", fg="yellow")
98
+ for e in orphans:
99
+ label = (
100
+ f"{e['name']}@{e['version']}"
101
+ if e["is_versioned"]
102
+ else f"{e['name']} (editable)"
103
+ )
104
+ click.echo(f" - {e['namespace']}/{label}")
105
+
106
+ click.echo()
107
+ if not yes and not click.confirm("Remove all of the above?"):
108
+ click.echo("❎ Cancelled.")
109
+ raise SystemExit(0)
110
+
111
+ for e in orphans:
112
+ shutil.rmtree(e["path"])
113
+
114
+ click.secho(f"🧹 Pruned {len(orphans)} orphaned cache entry/entries.", fg="green")
115
+
116
+ removed = compose.remove_broken_symlinks(workspace)
117
+ if removed:
118
+ click.secho(f"🔗 Removed {removed} broken feature symlink(s).", fg="yellow")
119
+ else:
120
+ click.secho("✅ No broken symlinks found.", fg="green")
121
+
122
+
123
+ cli_command = cache_prune
@@ -0,0 +1,57 @@
1
+ from splent_cli.services import context
2
+ import click
3
+ from pathlib import Path
4
+
5
+
6
+ def _dir_size(path: Path) -> int:
7
+ return sum(f.stat().st_size for f in path.rglob("*") if f.is_file())
8
+
9
+
10
+ def _human(size: int) -> str:
11
+ for unit in ("B", "KB", "MB", "GB"):
12
+ if size < 1024:
13
+ return f"{size:.1f} {unit}"
14
+ size /= 1024
15
+ return f"{size:.1f} TB"
16
+
17
+
18
+ @click.command("cache:size", short_help="Show disk usage of the feature cache.")
19
+ def cache_size():
20
+ """Shows disk usage per namespace and feature entry in the cache."""
21
+ workspace = context.workspace()
22
+ cache_root = workspace / ".splent_cache" / "features"
23
+
24
+ if not cache_root.exists():
25
+ click.secho("ℹ️ Feature cache is empty.", fg="yellow")
26
+ return
27
+
28
+ total = 0
29
+ namespaces = sorted(d for d in cache_root.iterdir() if d.is_dir())
30
+
31
+ if not namespaces:
32
+ click.secho("ℹ️ Feature cache is empty.", fg="yellow")
33
+ return
34
+
35
+ for ns_dir in namespaces:
36
+ ns_size = _dir_size(ns_dir)
37
+ total += ns_size
38
+ click.secho(f" {ns_dir.name} {_human(ns_size)}", bold=True)
39
+
40
+ entries = sorted(d for d in ns_dir.iterdir() if d.is_dir())
41
+ for i, feat_dir in enumerate(entries):
42
+ size = _dir_size(feat_dir)
43
+ connector = "└──" if i == len(entries) - 1 else "├──"
44
+ name = feat_dir.name
45
+ label = (
46
+ click.style(f"@{name.split('@')[1]}", fg="green")
47
+ if "@" in name
48
+ else click.style("editable", fg="blue")
49
+ )
50
+ base = name.split("@")[0] if "@" in name else name
51
+ click.echo(f" {connector} {base} {label} {_human(size)}")
52
+ click.echo()
53
+
54
+ click.secho(f" Total: {_human(total)}", fg="cyan", bold=True)
55
+
56
+
57
+ cli_command = cache_size