splent-cli 1.6.0__tar.gz → 1.8.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.
- {splent_cli-1.6.0/src/splent_cli.egg-info → splent_cli-1.8.0}/PKG-INFO +2 -1
- {splent_cli-1.6.0 → splent_cli-1.8.0}/pyproject.toml +2 -1
- {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/check/check_pyproject.py +53 -14
- {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/database/db_reset.py +4 -0
- {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/database/db_upgrade.py +5 -0
- {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/export/export_puml.py +100 -25
- {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/feature/feature_add.py +19 -0
- {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/feature/feature_attach.py +21 -0
- {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/feature/feature_env.py +12 -14
- splent_cli-1.8.0/src/splent_cli/commands/feature/feature_impact.py +161 -0
- splent_cli-1.8.0/src/splent_cli/commands/feature/feature_install.py +425 -0
- {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/feature/feature_release.py +22 -9
- {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/feature/feature_test.py +48 -0
- {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/product/product_build.py +153 -11
- {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/product/product_configure.py +66 -3
- splent_cli-1.8.0/src/splent_cli/commands/product/product_containers.py +316 -0
- {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/product/product_deploy.py +60 -6
- {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/product/product_env.py +33 -0
- {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/product/product_restart.py +36 -0
- {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/product/product_test.py +14 -0
- {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/product/product_up.py +12 -10
- splent_cli-1.8.0/src/splent_cli/services/preflight.py +207 -0
- {splent_cli-1.6.0 → splent_cli-1.8.0/src/splent_cli.egg-info}/PKG-INFO +2 -1
- {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli.egg-info/SOURCES.txt +2 -0
- {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli.egg-info/requires.txt +1 -0
- splent_cli-1.6.0/src/splent_cli/commands/product/product_containers.py +0 -196
- splent_cli-1.6.0/src/splent_cli/services/preflight.py +0 -88
- {splent_cli-1.6.0 → splent_cli-1.8.0}/LICENSE +0 -0
- {splent_cli-1.6.0 → splent_cli-1.8.0}/README.md +0 -0
- {splent_cli-1.6.0 → splent_cli-1.8.0}/setup.cfg +0 -0
- {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/__init__.py +0 -0
- {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/__main__.py +0 -0
- {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/cli.py +0 -0
- {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/__init__.py +0 -0
- {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/cache/__init__.py +0 -0
- {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/cache/cache_clear.py +0 -0
- {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/cache/cache_orphans.py +0 -0
- {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/cache/cache_outdated.py +0 -0
- {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/cache/cache_prune.py +0 -0
- {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/cache/cache_size.py +0 -0
- {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/cache/cache_status.py +0 -0
- {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/cache/cache_usage.py +0 -0
- {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/cache/cache_versions.py +0 -0
- {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/check/__init__.py +0 -0
- {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/check/check_deps.py +0 -0
- {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/check/check_docker.py +0 -0
- {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/check/check_env.py +0 -0
- {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/check/check_features.py +0 -0
- {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/check/check_github.py +0 -0
- {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/check/check_infra.py +0 -0
- {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/check/check_product.py +0 -0
- {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/check/check_pypi.py +0 -0
- {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/clear/__init__.py +0 -0
- {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/clear/clear_build.py +0 -0
- {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/clear/clear_log.py +0 -0
- {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/clear/clear_uploads.py +0 -0
- {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/command/__init__.py +0 -0
- {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/command/command_create.py +0 -0
- {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/coverage.py +0 -0
- {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/database/db_console.py +0 -0
- {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/database/db_dump.py +0 -0
- {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/database/db_migrate.py +0 -0
- {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/database/db_restore.py +0 -0
- {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/database/db_rollback.py +0 -0
- {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/database/db_seed.py +0 -0
- {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/database/db_status.py +0 -0
- {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/doctor.py +0 -0
- {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/env/env_list.py +0 -0
- {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/env/env_set.py +0 -0
- {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/env/env_show.py +0 -0
- {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/export/__init__.py +0 -0
- {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/feature/feature_clean.py +0 -0
- {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/feature/feature_clone.py +0 -0
- {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/feature/feature_compat.py +0 -0
- {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/feature/feature_compile.py +0 -0
- {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/feature/feature_contract.py +0 -0
- {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/feature/feature_create.py +0 -0
- {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/feature/feature_delete.py +0 -0
- {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/feature/feature_detach.py +0 -0
- {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/feature/feature_discard.py +0 -0
- {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/feature/feature_drift.py +0 -0
- {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/feature/feature_fork.py +0 -0
- {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/feature/feature_git.py +0 -0
- {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/feature/feature_hook_add.py +0 -0
- {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/feature/feature_hook_remove.py +0 -0
- {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/feature/feature_hooks.py +0 -0
- {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/feature/feature_inject_config.py +0 -0
- {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/feature/feature_list.py +0 -0
- {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/feature/feature_order.py +0 -0
- {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/feature/feature_outdated.py +0 -0
- {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/feature/feature_pin.py +0 -0
- {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/feature/feature_pip_install.py +0 -0
- {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/feature/feature_pull.py +0 -0
- {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/feature/feature_refine.py +0 -0
- {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/feature/feature_remove.py +0 -0
- {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/feature/feature_rename.py +0 -0
- {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/feature/feature_search.py +0 -0
- {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/feature/feature_status.py +0 -0
- {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/feature/feature_sync_template.py +0 -0
- {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/feature/feature_translate.py +0 -0
- {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/feature/feature_unlock.py +0 -0
- {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/feature/feature_upgrade.py +0 -0
- {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/feature/feature_versions.py +0 -0
- {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/feature/feature_xray.py +0 -0
- {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/lint.py +0 -0
- {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/locust.py +0 -0
- {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/product/__init__.py +0 -0
- {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/product/product_auto_require.py +0 -0
- {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/product/product_clean.py +0 -0
- {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/product/product_commands.py +0 -0
- {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/product/product_config.py +0 -0
- {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/product/product_console.py +0 -0
- {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/product/product_create.py +0 -0
- {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/product/product_derive.py +0 -0
- {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/product/product_deselect.py +0 -0
- {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/product/product_down.py +0 -0
- {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/product/product_drift.py +0 -0
- {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/product/product_list.py +0 -0
- {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/product/product_logs.py +0 -0
- {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/product/product_missing.py +0 -0
- {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/product/product_port.py +0 -0
- {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/product/product_release.py +0 -0
- {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/product/product_resolve.py +0 -0
- {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/product/product_routes.py +0 -0
- {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/product/product_run.py +0 -0
- {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/product/product_select.py +0 -0
- {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/product/product_shell.py +0 -0
- {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/product/product_signals.py +0 -0
- {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/product/product_sync_template.py +0 -0
- {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/product/product_validate.py +0 -0
- {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/release/__init__.py +0 -0
- {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/release/release_core.py +0 -0
- {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/selenium.py +0 -0
- {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/spl/__init__.py +0 -0
- {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/spl/spl_add_constraints.py +0 -0
- {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/spl/spl_add_feature.py +0 -0
- {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/spl/spl_configurations.py +0 -0
- {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/spl/spl_create.py +0 -0
- {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/spl/spl_deps.py +0 -0
- {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/spl/spl_features.py +0 -0
- {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/spl/spl_fetch.py +0 -0
- {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/spl/spl_info.py +0 -0
- {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/spl/spl_list.py +0 -0
- {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/spl/spl_utils.py +0 -0
- {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/tokens_setup.py +0 -0
- {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/uvl/uvl_utils.py +0 -0
- {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/commands/version.py +0 -0
- {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/services/__init__.py +0 -0
- {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/services/compose.py +0 -0
- {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/services/context.py +0 -0
- {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/services/release.py +0 -0
- {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/utils/__init__.py +0 -0
- {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/utils/cache_utils.py +0 -0
- {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/utils/command_loader.py +0 -0
- {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/utils/contract_freshness.py +0 -0
- {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/utils/db_utils.py +0 -0
- {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/utils/decorators.py +0 -0
- {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/utils/dynamic_imports.py +0 -0
- {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/utils/feature_installer.py +0 -0
- {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/utils/feature_utils.py +0 -0
- {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/utils/git_url.py +0 -0
- {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/utils/integrity.py +0 -0
- {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/utils/lifecycle.py +0 -0
- {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/utils/manifest.py +0 -0
- {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/utils/path_utils.py +0 -0
- {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli/utils/template_drift.py +0 -0
- {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli.egg-info/dependency_links.txt +0 -0
- {splent_cli-1.6.0 → splent_cli-1.8.0}/src/splent_cli.egg-info/entry_points.txt +0 -0
- {splent_cli-1.6.0 → splent_cli-1.8.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.
|
|
3
|
+
Version: 1.8.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
|
|
@@ -10,6 +10,7 @@ Description-Content-Type: text/markdown
|
|
|
10
10
|
License-File: LICENSE
|
|
11
11
|
Requires-Dist: click==8.1.8
|
|
12
12
|
Requires-Dist: packaging>=24.0
|
|
13
|
+
Requires-Dist: requests>=2.31.0
|
|
13
14
|
Provides-Extra: dev
|
|
14
15
|
Requires-Dist: setuptools==80.3.1; extra == "dev"
|
|
15
16
|
Requires-Dist: pytest==8.3.4; extra == "dev"
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "splent_cli"
|
|
7
|
-
version = "1.
|
|
7
|
+
version = "1.8.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"
|
|
@@ -14,6 +14,7 @@ license-files = ["LICENSE"]
|
|
|
14
14
|
dependencies = [
|
|
15
15
|
"click==8.1.8",
|
|
16
16
|
"packaging>=24.0",
|
|
17
|
+
"requests>=2.31.0",
|
|
17
18
|
]
|
|
18
19
|
|
|
19
20
|
[project.optional-dependencies]
|
|
@@ -9,7 +9,6 @@ import click
|
|
|
9
9
|
import tomllib
|
|
10
10
|
|
|
11
11
|
from splent_cli.services import context
|
|
12
|
-
from splent_cli.utils.feature_utils import read_features_from_data
|
|
13
12
|
|
|
14
13
|
|
|
15
14
|
def _find_missing_pkgs(deps: list) -> list:
|
|
@@ -88,29 +87,69 @@ def check_pyproject():
|
|
|
88
87
|
_ok(f"All {len(deps)} dependencies satisfied")
|
|
89
88
|
|
|
90
89
|
# Features
|
|
91
|
-
features = read_features_from_data(data)
|
|
92
|
-
_ok(f"{len(features)} features declared in [tool.splent.features]")
|
|
93
|
-
|
|
94
90
|
splent = data.get("tool", {}).get("splent", {})
|
|
91
|
+
base_feats = splent.get("features", [])
|
|
95
92
|
dev_feats = splent.get("features_dev", [])
|
|
96
93
|
prod_feats = splent.get("features_prod", [])
|
|
94
|
+
|
|
95
|
+
_ok(f"{len(base_feats)} features in [tool.splent].features")
|
|
97
96
|
if dev_feats:
|
|
98
97
|
_ok(f"{len(dev_feats)} dev-only features")
|
|
99
98
|
if prod_feats:
|
|
100
99
|
_ok(f"{len(prod_feats)} prod-only features")
|
|
101
100
|
|
|
102
|
-
#
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
101
|
+
# Check for duplicates across feature lists
|
|
102
|
+
def _bare_name(entry):
|
|
103
|
+
name = entry.split("/")[-1] if "/" in entry else entry
|
|
104
|
+
return name.split("@")[0]
|
|
105
|
+
|
|
106
|
+
all_entries = base_feats + dev_feats + prod_feats
|
|
107
|
+
seen_names = {}
|
|
108
|
+
for entry in all_entries:
|
|
109
|
+
bare = _bare_name(entry)
|
|
110
|
+
seen_names.setdefault(bare, []).append(entry)
|
|
111
|
+
|
|
112
|
+
for bare, entries in seen_names.items():
|
|
113
|
+
if len(entries) > 1:
|
|
114
|
+
short = bare.replace("splent_feature_", "")
|
|
115
|
+
_fail(f"Duplicate feature '{short}': {', '.join(entries)}")
|
|
116
|
+
|
|
117
|
+
# Check for inconsistent namespaces
|
|
118
|
+
namespaces = set()
|
|
119
|
+
for entry in all_entries:
|
|
120
|
+
if "/" in entry:
|
|
121
|
+
ns = entry.split("/")[0]
|
|
122
|
+
namespaces.add(ns)
|
|
123
|
+
if len(namespaces) > 1:
|
|
124
|
+
_warn(
|
|
125
|
+
f"Inconsistent namespaces: {', '.join(sorted(namespaces))} — use one format consistently"
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
# SPL / UVL
|
|
129
|
+
spl_name = splent.get("spl")
|
|
130
|
+
if spl_name:
|
|
131
|
+
uvl_path = os.path.join(
|
|
132
|
+
workspace, "splent_catalog", spl_name, f"{spl_name}.uvl"
|
|
133
|
+
)
|
|
134
|
+
if os.path.isfile(uvl_path):
|
|
135
|
+
_ok(f"SPL catalog: {spl_name} (UVL found)")
|
|
110
136
|
else:
|
|
111
|
-
_fail(
|
|
137
|
+
_fail(
|
|
138
|
+
f"SPL catalog: {spl_name} — UVL not found at splent_catalog/{spl_name}/{spl_name}.uvl"
|
|
139
|
+
)
|
|
112
140
|
else:
|
|
113
|
-
|
|
141
|
+
# Legacy fallback
|
|
142
|
+
uvl_cfg = splent.get("uvl", {})
|
|
143
|
+
uvl_file = uvl_cfg.get("file")
|
|
144
|
+
if uvl_file:
|
|
145
|
+
app_path = os.path.join(workspace, product)
|
|
146
|
+
uvl_path = os.path.join(app_path, "uvl", uvl_file)
|
|
147
|
+
if os.path.exists(uvl_path):
|
|
148
|
+
_ok(f"UVL file found: uvl/{uvl_file} (legacy)")
|
|
149
|
+
else:
|
|
150
|
+
_fail(f"UVL file not found: uvl/{uvl_file}")
|
|
151
|
+
else:
|
|
152
|
+
_warn("No SPL configured — set [tool.splent].spl in pyproject.toml")
|
|
114
153
|
|
|
115
154
|
click.echo()
|
|
116
155
|
if fail:
|
|
@@ -123,6 +123,10 @@ def db_reset(yes):
|
|
|
123
123
|
name=name,
|
|
124
124
|
version=version,
|
|
125
125
|
)
|
|
126
|
+
except ImportError as e:
|
|
127
|
+
if "models" in str(e):
|
|
128
|
+
continue
|
|
129
|
+
click.echo(click.style(f" ❌ {feat}: {e}", fg="red"))
|
|
126
130
|
except Exception as e:
|
|
127
131
|
click.echo(click.style(f" ❌ {feat}: {e}", fg="red"))
|
|
128
132
|
|
|
@@ -84,6 +84,11 @@ def db_upgrade(feature):
|
|
|
84
84
|
name=name,
|
|
85
85
|
version=version,
|
|
86
86
|
)
|
|
87
|
+
except ImportError as e:
|
|
88
|
+
if "models" in str(e):
|
|
89
|
+
# Feature has migrations/ dir but no models module — skip silently
|
|
90
|
+
continue
|
|
91
|
+
click.echo(click.style(f" ❌ {feat}: {e}", fg="red"))
|
|
87
92
|
except Exception as e:
|
|
88
93
|
click.echo(click.style(f" ❌ {feat}: {e}", fg="red"))
|
|
89
94
|
|
|
@@ -149,8 +149,8 @@ def _parse_models(models_path: str) -> list[dict]:
|
|
|
149
149
|
text = "\n".join(joined_lines)
|
|
150
150
|
|
|
151
151
|
classes = []
|
|
152
|
-
# Split by class definition
|
|
153
|
-
class_blocks = re.split(r"^(class \w
|
|
152
|
+
# Split by class definition (with or without parentheses for mixins)
|
|
153
|
+
class_blocks = re.split(r"^(class \w+(?:\(.*?\))?:)", text, flags=re.MULTILINE)
|
|
154
154
|
|
|
155
155
|
i = 1
|
|
156
156
|
while i < len(class_blocks):
|
|
@@ -158,7 +158,7 @@ def _parse_models(models_path: str) -> list[dict]:
|
|
|
158
158
|
body = class_blocks[i + 1] if i + 1 < len(class_blocks) else ""
|
|
159
159
|
i += 2
|
|
160
160
|
|
|
161
|
-
class_m = re.match(r"class (\w+)
|
|
161
|
+
class_m = re.match(r"class (\w+)", header)
|
|
162
162
|
if not class_m:
|
|
163
163
|
continue
|
|
164
164
|
class_name = class_m.group(1)
|
|
@@ -213,8 +213,11 @@ def _parse_models(models_path: str) -> list[dict]:
|
|
|
213
213
|
if rel_m:
|
|
214
214
|
rel_name = rel_m.group(1)
|
|
215
215
|
rel_args = rel_m.group(2)
|
|
216
|
-
|
|
217
|
-
|
|
216
|
+
# Target can be unquoted (User) or quoted ("User" / 'User')
|
|
217
|
+
target_m = re.match(r"""[\"']?(\w+)[\"']?""", rel_args)
|
|
218
|
+
target = target_m.group(1) if target_m else None
|
|
219
|
+
if not target:
|
|
220
|
+
continue
|
|
218
221
|
uselist = "uselist=False" not in rel_args
|
|
219
222
|
relationships.append(
|
|
220
223
|
{
|
|
@@ -668,11 +671,16 @@ def _render_exports(
|
|
|
668
671
|
)
|
|
669
672
|
except subprocess.CalledProcessError as e:
|
|
670
673
|
click.secho("❌ PlantUML failed to render diagram.", fg="red")
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
click.secho(stderr
|
|
674
|
+
stderr = e.stderr
|
|
675
|
+
if isinstance(stderr, bytes):
|
|
676
|
+
stderr = stderr.decode(errors="replace")
|
|
677
|
+
if stderr:
|
|
678
|
+
click.secho(stderr.strip(), fg="bright_black")
|
|
679
|
+
stdout = e.stdout
|
|
680
|
+
if isinstance(stdout, bytes):
|
|
681
|
+
stdout = stdout.decode(errors="replace")
|
|
682
|
+
if stdout:
|
|
683
|
+
click.secho(stdout.strip(), fg="bright_black")
|
|
676
684
|
return
|
|
677
685
|
|
|
678
686
|
try:
|
|
@@ -780,21 +788,41 @@ def export_puml(
|
|
|
780
788
|
product = context.require_app()
|
|
781
789
|
product_dir = os.path.join(workspace, product)
|
|
782
790
|
|
|
783
|
-
# Read UVL
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
791
|
+
# Read UVL — try SPL catalog first, fall back to legacy [tool.splent.uvl]
|
|
792
|
+
import tomllib
|
|
793
|
+
|
|
794
|
+
uvl_path = None
|
|
795
|
+
pyproject_path = os.path.join(product_dir, "pyproject.toml")
|
|
796
|
+
if os.path.isfile(pyproject_path):
|
|
797
|
+
with open(pyproject_path, "rb") as f:
|
|
798
|
+
pydata = tomllib.load(f)
|
|
799
|
+
spl_name = pydata.get("tool", {}).get("splent", {}).get("spl")
|
|
800
|
+
if spl_name:
|
|
801
|
+
candidate = os.path.join(
|
|
802
|
+
workspace, "splent_catalog", spl_name, f"{spl_name}.uvl"
|
|
803
|
+
)
|
|
804
|
+
if os.path.isfile(candidate):
|
|
805
|
+
uvl_path = candidate
|
|
806
|
+
|
|
807
|
+
# Legacy fallback: [tool.splent.uvl].file
|
|
808
|
+
if not uvl_path:
|
|
809
|
+
try:
|
|
810
|
+
uvl_cfg = PyprojectReader.for_product(product_dir).uvl_config
|
|
811
|
+
uvl_file = uvl_cfg.get("file")
|
|
812
|
+
if uvl_file:
|
|
813
|
+
candidate = os.path.join(product_dir, "uvl", uvl_file)
|
|
814
|
+
if os.path.isfile(candidate):
|
|
815
|
+
uvl_path = candidate
|
|
816
|
+
except (FileNotFoundError, RuntimeError):
|
|
817
|
+
pass
|
|
818
|
+
|
|
819
|
+
if not uvl_path:
|
|
820
|
+
click.secho(
|
|
821
|
+
"❌ No UVL file found.\n"
|
|
822
|
+
" Set [tool.splent].spl in pyproject.toml (SPL catalog)\n"
|
|
823
|
+
" or [tool.splent.uvl].file (legacy).",
|
|
824
|
+
fg="red",
|
|
825
|
+
)
|
|
798
826
|
raise SystemExit(1)
|
|
799
827
|
|
|
800
828
|
click.echo(f"📖 Reading UVL model: {uvl_path}")
|
|
@@ -848,6 +876,53 @@ def export_puml(
|
|
|
848
876
|
if parsed:
|
|
849
877
|
all_models[package] = parsed
|
|
850
878
|
break
|
|
879
|
+
# Apply refinement mixins: merge mixin attributes into target models
|
|
880
|
+
for package, fpath in feature_paths.items():
|
|
881
|
+
pyproject_path = os.path.join(fpath, "pyproject.toml")
|
|
882
|
+
if not os.path.isfile(pyproject_path):
|
|
883
|
+
continue
|
|
884
|
+
import tomllib as _tomllib
|
|
885
|
+
|
|
886
|
+
with open(pyproject_path, "rb") as f:
|
|
887
|
+
feat_data = _tomllib.load(f)
|
|
888
|
+
extends = (
|
|
889
|
+
feat_data.get("tool", {})
|
|
890
|
+
.get("splent", {})
|
|
891
|
+
.get("refinement", {})
|
|
892
|
+
.get("extends", {})
|
|
893
|
+
.get("models", [])
|
|
894
|
+
)
|
|
895
|
+
if not extends:
|
|
896
|
+
continue
|
|
897
|
+
# Parse the mixin's models.py
|
|
898
|
+
mixin_models = []
|
|
899
|
+
src_root = os.path.join(fpath, "src")
|
|
900
|
+
if os.path.isdir(src_root):
|
|
901
|
+
for org_dir in os.listdir(src_root):
|
|
902
|
+
mpath = os.path.join(src_root, org_dir, package, "models.py")
|
|
903
|
+
if os.path.isfile(mpath):
|
|
904
|
+
mixin_models = _parse_models(mpath)
|
|
905
|
+
break
|
|
906
|
+
# Merge each mixin into its target model
|
|
907
|
+
for ext in extends:
|
|
908
|
+
target_name = ext.get("target")
|
|
909
|
+
mixin_name = ext.get("mixin")
|
|
910
|
+
if not target_name or not mixin_name:
|
|
911
|
+
continue
|
|
912
|
+
# Find the mixin class in parsed mixin_models
|
|
913
|
+
mixin_cls = next(
|
|
914
|
+
(m for m in mixin_models if m["name"] == mixin_name), None
|
|
915
|
+
)
|
|
916
|
+
if not mixin_cls:
|
|
917
|
+
continue
|
|
918
|
+
# Find the target model across all parsed features
|
|
919
|
+
for feat_models in all_models.values():
|
|
920
|
+
for model in feat_models:
|
|
921
|
+
if model["name"] == target_name:
|
|
922
|
+
model["attributes"].extend(mixin_cls["attributes"])
|
|
923
|
+
model["methods"].extend(mixin_cls["methods"])
|
|
924
|
+
break
|
|
925
|
+
|
|
851
926
|
click.echo(
|
|
852
927
|
f"📋 Parsed models from {len(all_models)}/{len(feature_paths)} features."
|
|
853
928
|
)
|
|
@@ -59,6 +59,25 @@ def feature_add(full_name, env_scope):
|
|
|
59
59
|
)
|
|
60
60
|
raise SystemExit(1)
|
|
61
61
|
|
|
62
|
+
# ── Auto-detect env scope from feature contract ───────────────────
|
|
63
|
+
if not env_scope:
|
|
64
|
+
feat_pyproject = os.path.join(feature_dir, "pyproject.toml")
|
|
65
|
+
if os.path.isfile(feat_pyproject):
|
|
66
|
+
with open(feat_pyproject, "rb") as f:
|
|
67
|
+
feat_data = tomllib.load(f)
|
|
68
|
+
contract_env = (
|
|
69
|
+
feat_data.get("tool", {})
|
|
70
|
+
.get("splent", {})
|
|
71
|
+
.get("contract", {})
|
|
72
|
+
.get("env")
|
|
73
|
+
)
|
|
74
|
+
if contract_env:
|
|
75
|
+
env_scope = contract_env
|
|
76
|
+
click.echo(
|
|
77
|
+
click.style(" scope ", dim=True)
|
|
78
|
+
+ f"contract declares env={contract_env} → features_{contract_env}"
|
|
79
|
+
)
|
|
80
|
+
|
|
62
81
|
# ── Update pyproject.toml ─────────────────────────────────────────
|
|
63
82
|
pyproject_path = os.path.join(workspace, product, "pyproject.toml")
|
|
64
83
|
if not os.path.exists(pyproject_path):
|
|
@@ -64,6 +64,27 @@ def feature_attach(feature_identifier, version, env_scope):
|
|
|
64
64
|
|
|
65
65
|
short = feature_name.replace("splent_feature_", "")
|
|
66
66
|
|
|
67
|
+
# ── Auto-detect env scope from feature contract ───────────────────
|
|
68
|
+
if not env_scope:
|
|
69
|
+
feat_pyproject = os.path.join(versioned_dir, "pyproject.toml")
|
|
70
|
+
if os.path.isfile(feat_pyproject):
|
|
71
|
+
import tomllib as _tomllib
|
|
72
|
+
|
|
73
|
+
with open(feat_pyproject, "rb") as f:
|
|
74
|
+
feat_data = _tomllib.load(f)
|
|
75
|
+
contract_env = (
|
|
76
|
+
feat_data.get("tool", {})
|
|
77
|
+
.get("splent", {})
|
|
78
|
+
.get("contract", {})
|
|
79
|
+
.get("env")
|
|
80
|
+
)
|
|
81
|
+
if contract_env:
|
|
82
|
+
env_scope = contract_env
|
|
83
|
+
click.echo(
|
|
84
|
+
click.style(" scope ", dim=True)
|
|
85
|
+
+ f"contract declares env={contract_env} → features_{contract_env}"
|
|
86
|
+
)
|
|
87
|
+
|
|
67
88
|
# ── Update pyproject.toml ─────────────────────────────────────────
|
|
68
89
|
full_name = f"{namespace}/{feature_name}@{version}"
|
|
69
90
|
bare_name = f"{namespace}/{feature_name}"
|
|
@@ -45,19 +45,26 @@ def feature_env(feature_name, generate, env_name):
|
|
|
45
45
|
with open(pyproject_path, "rb") as f:
|
|
46
46
|
data = tomllib.load(f)
|
|
47
47
|
|
|
48
|
-
|
|
48
|
+
# 2️⃣ Determinar entorno (antes de leer features, para incluir features_dev/prod)
|
|
49
|
+
env_from_var = os.getenv("SPLENT_ENV")
|
|
50
|
+
if not env_name and env_from_var:
|
|
51
|
+
env_name = env_from_var
|
|
52
|
+
click.echo(f"🌍 Using SPLENT_ENV={env_name}")
|
|
53
|
+
elif not env_name:
|
|
54
|
+
click.echo("❌ You must specify --dev, --prod, or define SPLENT_ENV=dev|prod.")
|
|
55
|
+
raise SystemExit(1)
|
|
56
|
+
|
|
57
|
+
feature_entries = read_features_from_data(data, env_name)
|
|
49
58
|
if not feature_entries:
|
|
50
59
|
click.echo("❌ No features declared in pyproject.toml.")
|
|
51
60
|
raise SystemExit(1)
|
|
52
61
|
|
|
53
|
-
#
|
|
62
|
+
# 3️⃣ Buscar la feature pedida con su versión
|
|
54
63
|
feature_entry = next(
|
|
55
64
|
(f for f in feature_entries if f.startswith(feature_name)), None
|
|
56
65
|
)
|
|
57
66
|
if not feature_entry:
|
|
58
|
-
click.echo(
|
|
59
|
-
f"❌ Feature '{feature_name}' not found in [features] section of {pyproject_path}"
|
|
60
|
-
)
|
|
67
|
+
click.echo(f"❌ Feature '{feature_name}' not found in pyproject.toml")
|
|
61
68
|
raise SystemExit(1)
|
|
62
69
|
|
|
63
70
|
# Derive org_safe from the entry itself (e.g. "splent_io/splent_feature_auth@v1")
|
|
@@ -68,15 +75,6 @@ def feature_env(feature_name, generate, env_name):
|
|
|
68
75
|
org_safe = "splent_io"
|
|
69
76
|
entry_basename = feature_entry
|
|
70
77
|
|
|
71
|
-
# 3️⃣ Determinar entorno
|
|
72
|
-
env_from_var = os.getenv("SPLENT_ENV")
|
|
73
|
-
if not env_name and env_from_var:
|
|
74
|
-
env_name = env_from_var
|
|
75
|
-
click.echo(f"🌍 Using SPLENT_ENV={env_name}")
|
|
76
|
-
elif not env_name:
|
|
77
|
-
click.echo("❌ You must specify --dev, --prod, or define SPLENT_ENV=dev|prod.")
|
|
78
|
-
raise SystemExit(1)
|
|
79
|
-
|
|
80
78
|
# 4️⃣ Ruta del symlink del producto (con versión incluida)
|
|
81
79
|
symlink_path = os.path.join(
|
|
82
80
|
workspace, product, "features", org_safe, entry_basename
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"""
|
|
2
|
+
splent feature:impact
|
|
3
|
+
|
|
4
|
+
Shows the full dependency impact of a feature within the active product:
|
|
5
|
+
what it requires, what depends on it, and what would break if removed.
|
|
6
|
+
|
|
7
|
+
All dependency information is resolved from the product's UVL constraints.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
|
|
12
|
+
import click
|
|
13
|
+
|
|
14
|
+
from splent_cli.services import context
|
|
15
|
+
from splent_cli.commands.feature.feature_order import (
|
|
16
|
+
_uvl_path,
|
|
17
|
+
_build_requires_map,
|
|
18
|
+
_build_reverse_map,
|
|
19
|
+
_closure,
|
|
20
|
+
_parse_entry,
|
|
21
|
+
)
|
|
22
|
+
from splent_framework.utils.pyproject_reader import PyprojectReader
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _short_name(pkg: str) -> str:
|
|
26
|
+
"""splent_feature_auth → auth"""
|
|
27
|
+
return pkg.removeprefix("splent_feature_")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@click.command(
|
|
31
|
+
"feature:impact",
|
|
32
|
+
short_help="Show the dependency impact of adding or removing a feature.",
|
|
33
|
+
)
|
|
34
|
+
@click.argument("feature_ref", required=True)
|
|
35
|
+
def feature_impact(feature_ref):
|
|
36
|
+
"""
|
|
37
|
+
Show what FEATURE_REF depends on and what depends on it.
|
|
38
|
+
|
|
39
|
+
Reads the product's UVL constraints to build a bidirectional dependency
|
|
40
|
+
graph and shows:
|
|
41
|
+
|
|
42
|
+
\b
|
|
43
|
+
1. Dependencies — features this one requires to work.
|
|
44
|
+
2. Dependents — features that require this one.
|
|
45
|
+
3. Impact — transitive set of features that would break
|
|
46
|
+
if this feature were removed.
|
|
47
|
+
|
|
48
|
+
FEATURE_REF can be a short name (auth) or full package name
|
|
49
|
+
(splent_feature_auth).
|
|
50
|
+
|
|
51
|
+
\b
|
|
52
|
+
Examples:
|
|
53
|
+
splent feature:impact auth
|
|
54
|
+
splent feature:impact splent_feature_mail
|
|
55
|
+
"""
|
|
56
|
+
product = context.require_app()
|
|
57
|
+
workspace = str(context.workspace())
|
|
58
|
+
product_dir = os.path.join(workspace, product)
|
|
59
|
+
|
|
60
|
+
try:
|
|
61
|
+
reader = PyprojectReader.for_product(product_dir)
|
|
62
|
+
env = os.getenv("SPLENT_ENV")
|
|
63
|
+
features_raw = reader.features_for_env(env)
|
|
64
|
+
except FileNotFoundError:
|
|
65
|
+
click.secho(" pyproject.toml not found.", fg="red")
|
|
66
|
+
raise SystemExit(1)
|
|
67
|
+
|
|
68
|
+
uvl = _uvl_path(product_dir)
|
|
69
|
+
if not uvl or not os.path.isfile(uvl):
|
|
70
|
+
click.secho(" No UVL file found — cannot compute impact.", fg="red")
|
|
71
|
+
raise SystemExit(1)
|
|
72
|
+
|
|
73
|
+
# Normalize input: accept "auth" or "splent_feature_auth"
|
|
74
|
+
target = feature_ref
|
|
75
|
+
if target.startswith("splent_feature_"):
|
|
76
|
+
target = target[len("splent_feature_") :]
|
|
77
|
+
pkg_target = f"splent_feature_{target}"
|
|
78
|
+
|
|
79
|
+
# Build declared feature set
|
|
80
|
+
declared_pkgs = set()
|
|
81
|
+
for entry in features_raw:
|
|
82
|
+
_, name, _ = _parse_entry(entry)
|
|
83
|
+
declared_pkgs.add(name)
|
|
84
|
+
|
|
85
|
+
if pkg_target not in declared_pkgs:
|
|
86
|
+
click.secho(f" '{feature_ref}' is not declared in this product.", fg="red")
|
|
87
|
+
raise SystemExit(1)
|
|
88
|
+
|
|
89
|
+
# Build dependency graphs
|
|
90
|
+
requires_map = _build_requires_map(uvl)
|
|
91
|
+
reverse_map = _build_reverse_map(requires_map)
|
|
92
|
+
|
|
93
|
+
# Direct dependencies (what I need)
|
|
94
|
+
direct_deps = requires_map.get(pkg_target, [])
|
|
95
|
+
|
|
96
|
+
# Full dependency closure (transitive: what I need, and what they need, etc.)
|
|
97
|
+
all_deps = _closure(pkg_target, requires_map, declared_pkgs)
|
|
98
|
+
|
|
99
|
+
# Direct dependents (who needs me)
|
|
100
|
+
direct_dependents = reverse_map.get(pkg_target, [])
|
|
101
|
+
|
|
102
|
+
# Full reverse closure (transitive: who breaks if I'm removed)
|
|
103
|
+
all_affected = _closure(pkg_target, reverse_map, declared_pkgs)
|
|
104
|
+
|
|
105
|
+
# ── Output ────────────────────────────────────────────────────────
|
|
106
|
+
|
|
107
|
+
click.echo()
|
|
108
|
+
click.secho(f" feature:impact — {target}", bold=True)
|
|
109
|
+
click.echo(click.style(f" Product: {product}", fg="bright_black"))
|
|
110
|
+
click.echo(click.style(f" UVL: {os.path.basename(uvl)}", fg="bright_black"))
|
|
111
|
+
|
|
112
|
+
# Section 1: What I depend on
|
|
113
|
+
click.echo()
|
|
114
|
+
click.secho(" Depends on", bold=True)
|
|
115
|
+
|
|
116
|
+
if not all_deps:
|
|
117
|
+
click.echo(click.style(" (none) — this feature is independent", fg="green"))
|
|
118
|
+
else:
|
|
119
|
+
direct_set = set(direct_deps)
|
|
120
|
+
for dep in all_deps:
|
|
121
|
+
marker = "direct" if dep in direct_set else "transitive"
|
|
122
|
+
icon = "→" if marker == "direct" else " →"
|
|
123
|
+
color = "cyan" if marker == "direct" else "bright_black"
|
|
124
|
+
click.echo(
|
|
125
|
+
f" {icon} {click.style(_short_name(dep), fg=color)} ({marker})"
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
# Section 2: Who depends on me + impact
|
|
129
|
+
click.echo()
|
|
130
|
+
click.secho(" Depended on by", bold=True)
|
|
131
|
+
|
|
132
|
+
if not all_affected:
|
|
133
|
+
click.echo(click.style(" (none) — safe to remove", fg="green"))
|
|
134
|
+
else:
|
|
135
|
+
direct_set = set(direct_dependents)
|
|
136
|
+
for dep in all_affected:
|
|
137
|
+
marker = "direct" if dep in direct_set else "transitive"
|
|
138
|
+
icon = "←" if marker == "direct" else " ←"
|
|
139
|
+
color = "red" if marker == "direct" else "yellow"
|
|
140
|
+
click.echo(
|
|
141
|
+
f" {icon} {click.style(_short_name(dep), fg=color)} ({marker})"
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
# Section 3: Summary
|
|
145
|
+
click.echo()
|
|
146
|
+
if all_affected:
|
|
147
|
+
click.secho(
|
|
148
|
+
f" Removing {target} would break {len(all_affected)} feature(s).",
|
|
149
|
+
fg="red",
|
|
150
|
+
bold=True,
|
|
151
|
+
)
|
|
152
|
+
else:
|
|
153
|
+
click.secho(
|
|
154
|
+
f" Removing {target} has no impact on other features.",
|
|
155
|
+
fg="green",
|
|
156
|
+
bold=True,
|
|
157
|
+
)
|
|
158
|
+
click.echo()
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
cli_command = feature_impact
|