splent-cli 1.13.0__tar.gz → 1.14.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.13.0/src/splent_cli.egg-info → splent_cli-1.14.0}/PKG-INFO +1 -1
- {splent_cli-1.13.0 → splent_cli-1.14.0}/pyproject.toml +1 -1
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli/cli.py +3 -0
- splent_cli-1.14.0/src/splent_cli/commands/check/check_contracts.py +206 -0
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli/commands/feature/feature_clean.py +5 -35
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli/commands/feature/feature_clone.py +2 -35
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli/commands/feature/feature_contract.py +44 -1
- splent_cli-1.14.0/src/splent_cli/commands/feature/feature_info.py +214 -0
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli/commands/feature/feature_install.py +5 -24
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli/commands/feature/feature_outdated.py +5 -28
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli/commands/feature/feature_release.py +251 -4
- splent_cli-1.14.0/src/splent_cli/commands/feature/feature_search.py +238 -0
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli/commands/feature/feature_upgrade.py +9 -32
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli/commands/feature/feature_versions.py +13 -83
- splent_cli-1.14.0/src/splent_cli/commands/marketplace/marketplace_index.py +192 -0
- splent_cli-1.14.0/src/splent_cli/services/marketplace.py +543 -0
- splent_cli-1.14.0/src/splent_cli/services/registry.py +242 -0
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli/services/release.py +3 -34
- splent_cli-1.14.0/src/splent_cli/utils/archetype.py +52 -0
- {splent_cli-1.13.0 → splent_cli-1.14.0/src/splent_cli.egg-info}/PKG-INFO +1 -1
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli.egg-info/SOURCES.txt +6 -0
- splent_cli-1.13.0/src/splent_cli/commands/feature/feature_search.py +0 -144
- {splent_cli-1.13.0 → splent_cli-1.14.0}/LICENSE +0 -0
- {splent_cli-1.13.0 → splent_cli-1.14.0}/README.md +0 -0
- {splent_cli-1.13.0 → splent_cli-1.14.0}/setup.cfg +0 -0
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli/__init__.py +0 -0
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli/__main__.py +0 -0
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli/commands/__init__.py +0 -0
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli/commands/cache/__init__.py +0 -0
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli/commands/cache/cache_clear.py +0 -0
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli/commands/cache/cache_orphans.py +0 -0
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli/commands/cache/cache_outdated.py +0 -0
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli/commands/cache/cache_prune.py +0 -0
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli/commands/cache/cache_size.py +0 -0
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli/commands/cache/cache_status.py +0 -0
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli/commands/cache/cache_usage.py +0 -0
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli/commands/cache/cache_versions.py +0 -0
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli/commands/check/__init__.py +0 -0
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli/commands/check/check_deps.py +0 -0
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli/commands/check/check_docker.py +0 -0
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli/commands/check/check_env.py +0 -0
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli/commands/check/check_features.py +0 -0
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli/commands/check/check_github.py +0 -0
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli/commands/check/check_infra.py +0 -0
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli/commands/check/check_product.py +0 -0
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli/commands/check/check_pypi.py +0 -0
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli/commands/check/check_pyproject.py +0 -0
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli/commands/clear/__init__.py +0 -0
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli/commands/clear/clear_build.py +0 -0
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli/commands/clear/clear_log.py +0 -0
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli/commands/clear/clear_uploads.py +0 -0
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli/commands/command/__init__.py +0 -0
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli/commands/command/command_create.py +0 -0
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli/commands/coverage.py +0 -0
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli/commands/database/db_console.py +0 -0
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli/commands/database/db_dump.py +0 -0
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli/commands/database/db_migrate.py +0 -0
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli/commands/database/db_reset.py +0 -0
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli/commands/database/db_restore.py +0 -0
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli/commands/database/db_rollback.py +0 -0
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli/commands/database/db_seed.py +0 -0
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli/commands/database/db_status.py +0 -0
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli/commands/database/db_upgrade.py +0 -0
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli/commands/doctor.py +0 -0
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli/commands/env/env_list.py +0 -0
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli/commands/env/env_set.py +0 -0
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli/commands/env/env_show.py +0 -0
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli/commands/export/__init__.py +0 -0
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli/commands/export/export_puml.py +0 -0
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli/commands/feature/feature_add.py +0 -0
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli/commands/feature/feature_attach.py +0 -0
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli/commands/feature/feature_compat.py +0 -0
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli/commands/feature/feature_compile.py +0 -0
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli/commands/feature/feature_create.py +0 -0
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli/commands/feature/feature_delete.py +0 -0
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli/commands/feature/feature_detach.py +0 -0
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli/commands/feature/feature_discard.py +0 -0
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli/commands/feature/feature_drift.py +0 -0
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli/commands/feature/feature_env.py +0 -0
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli/commands/feature/feature_fork.py +0 -0
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli/commands/feature/feature_git.py +0 -0
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli/commands/feature/feature_hook_add.py +0 -0
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli/commands/feature/feature_hook_remove.py +0 -0
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli/commands/feature/feature_hooks.py +0 -0
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli/commands/feature/feature_impact.py +0 -0
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli/commands/feature/feature_inject_config.py +0 -0
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli/commands/feature/feature_list.py +0 -0
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli/commands/feature/feature_order.py +0 -0
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli/commands/feature/feature_pin.py +0 -0
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli/commands/feature/feature_pip_install.py +0 -0
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli/commands/feature/feature_pull.py +0 -0
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli/commands/feature/feature_refine.py +0 -0
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli/commands/feature/feature_remove.py +0 -0
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli/commands/feature/feature_rename.py +0 -0
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli/commands/feature/feature_status.py +0 -0
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli/commands/feature/feature_sync_template.py +0 -0
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli/commands/feature/feature_test.py +0 -0
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli/commands/feature/feature_translate.py +0 -0
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli/commands/feature/feature_unlock.py +0 -0
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli/commands/feature/feature_xray.py +0 -0
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli/commands/lint.py +0 -0
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli/commands/locust.py +0 -0
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli/commands/product/__init__.py +0 -0
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli/commands/product/product_auto_require.py +0 -0
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli/commands/product/product_build.py +0 -0
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli/commands/product/product_clean.py +0 -0
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli/commands/product/product_commands.py +0 -0
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli/commands/product/product_config.py +0 -0
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli/commands/product/product_configure.py +0 -0
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli/commands/product/product_console.py +0 -0
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli/commands/product/product_containers.py +0 -0
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli/commands/product/product_create.py +0 -0
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli/commands/product/product_deploy.py +0 -0
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli/commands/product/product_derive.py +0 -0
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli/commands/product/product_deselect.py +0 -0
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli/commands/product/product_down.py +0 -0
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli/commands/product/product_drift.py +0 -0
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli/commands/product/product_env.py +0 -0
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli/commands/product/product_list.py +0 -0
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli/commands/product/product_logs.py +0 -0
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli/commands/product/product_missing.py +0 -0
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli/commands/product/product_port.py +0 -0
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli/commands/product/product_release.py +0 -0
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli/commands/product/product_resolve.py +0 -0
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli/commands/product/product_restart.py +0 -0
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli/commands/product/product_routes.py +0 -0
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli/commands/product/product_run.py +0 -0
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli/commands/product/product_select.py +0 -0
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli/commands/product/product_shell.py +0 -0
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli/commands/product/product_signals.py +0 -0
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli/commands/product/product_sync_template.py +0 -0
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli/commands/product/product_test.py +0 -0
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli/commands/product/product_up.py +0 -0
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli/commands/product/product_validate.py +0 -0
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli/commands/release/__init__.py +0 -0
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli/commands/release/release_core.py +0 -0
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli/commands/selenium.py +0 -0
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli/commands/spl/__init__.py +0 -0
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli/commands/spl/spl_add_constraints.py +0 -0
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli/commands/spl/spl_add_feature.py +0 -0
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli/commands/spl/spl_compare.py +0 -0
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli/commands/spl/spl_configurations.py +0 -0
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli/commands/spl/spl_create.py +0 -0
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli/commands/spl/spl_deps.py +0 -0
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli/commands/spl/spl_features.py +0 -0
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli/commands/spl/spl_fetch.py +0 -0
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli/commands/spl/spl_info.py +0 -0
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli/commands/spl/spl_list.py +0 -0
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli/commands/spl/spl_utils.py +0 -0
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli/commands/tokens_setup.py +0 -0
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli/commands/uvl/uvl_utils.py +0 -0
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli/commands/version.py +0 -0
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli/services/__init__.py +0 -0
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli/services/compose.py +0 -0
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli/services/context.py +0 -0
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli/services/preflight.py +0 -0
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli/utils/__init__.py +0 -0
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli/utils/cache_utils.py +0 -0
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli/utils/command_loader.py +0 -0
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli/utils/contract_freshness.py +0 -0
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli/utils/db_utils.py +0 -0
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli/utils/decorators.py +0 -0
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli/utils/dynamic_imports.py +0 -0
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli/utils/feature_installer.py +0 -0
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli/utils/feature_utils.py +0 -0
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli/utils/git_url.py +0 -0
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli/utils/integrity.py +0 -0
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli/utils/io_utils.py +0 -0
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli/utils/lifecycle.py +0 -0
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli/utils/manifest.py +0 -0
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli/utils/path_utils.py +0 -0
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli/utils/proc.py +0 -0
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli/utils/template_drift.py +0 -0
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli.egg-info/dependency_links.txt +0 -0
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli.egg-info/entry_points.txt +0 -0
- {splent_cli-1.13.0 → splent_cli-1.14.0}/src/splent_cli.egg-info/requires.txt +0 -0
- {splent_cli-1.13.0 → splent_cli-1.14.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.14.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
|
|
@@ -109,6 +109,9 @@ class SPLENTCLI(click.Group):
|
|
|
109
109
|
"🧬 SPL & Variability": [
|
|
110
110
|
cmd for cmd in all_cmds if cmd.startswith(("spl:", "uvl:"))
|
|
111
111
|
],
|
|
112
|
+
"🛒 Marketplace": [
|
|
113
|
+
cmd for cmd in all_cmds if cmd.startswith("marketplace:")
|
|
114
|
+
],
|
|
112
115
|
"🧱 Database": [cmd for cmd in all_cmds if cmd.startswith("db:")],
|
|
113
116
|
"💾 Cache": [cmd for cmd in all_cmds if cmd.startswith("cache:")],
|
|
114
117
|
"🔍 Checks": [cmd for cmd in all_cmds if cmd.startswith("check:")],
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
"""
|
|
2
|
+
check:contracts — Verify that feature contracts and SPL UVL models agree.
|
|
3
|
+
|
|
4
|
+
The dependency truth must not be split: a feature's requires.features (in
|
|
5
|
+
[tool.splent.contract]) and the SPL's UVL implications (A => B) describe
|
|
6
|
+
the same reality from two sides. This check reports:
|
|
7
|
+
|
|
8
|
+
- contract dependencies with no matching UVL constraint (ERROR: the SPL
|
|
9
|
+
would let a product select A without B and break at runtime), and
|
|
10
|
+
- UVL implications not backed by any contract (WARNING: the dependency
|
|
11
|
+
is invisible to feature:install and the marketplace).
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
import click
|
|
17
|
+
import tomllib
|
|
18
|
+
|
|
19
|
+
from splent_cli.services import context, marketplace
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _snapshot_version_key(pyproject_path: Path) -> tuple:
|
|
23
|
+
"""Semver sort key for a cache snapshot dir like splent_feature_x@v1.10.0."""
|
|
24
|
+
import re
|
|
25
|
+
|
|
26
|
+
_, _, version = pyproject_path.parent.name.partition("@")
|
|
27
|
+
m = re.match(r"v?(\d+)\.(\d+)\.(\d+)", version)
|
|
28
|
+
if not m:
|
|
29
|
+
return (-1, -1, -1)
|
|
30
|
+
return (int(m.group(1)), int(m.group(2)), int(m.group(3)))
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _local_contract_requires(workspace: str, package: str) -> list[str] | None:
|
|
34
|
+
"""requires.features of a feature found locally (workspace root, else the
|
|
35
|
+
newest cache snapshot by semver). None when not available locally."""
|
|
36
|
+
candidates = [Path(workspace) / package / "pyproject.toml"]
|
|
37
|
+
cache = Path(workspace) / ".splent_cache" / "features"
|
|
38
|
+
if cache.is_dir():
|
|
39
|
+
candidates.extend(
|
|
40
|
+
sorted(
|
|
41
|
+
cache.glob(f"*/{package}@*/pyproject.toml"),
|
|
42
|
+
key=_snapshot_version_key,
|
|
43
|
+
reverse=True,
|
|
44
|
+
)
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
for path in candidates:
|
|
48
|
+
if not path.is_file():
|
|
49
|
+
continue
|
|
50
|
+
try:
|
|
51
|
+
data = tomllib.loads(path.read_text())
|
|
52
|
+
except (OSError, tomllib.TOMLDecodeError):
|
|
53
|
+
continue
|
|
54
|
+
requires = (
|
|
55
|
+
data.get("tool", {})
|
|
56
|
+
.get("splent", {})
|
|
57
|
+
.get("contract", {})
|
|
58
|
+
.get("requires", {})
|
|
59
|
+
)
|
|
60
|
+
return {
|
|
61
|
+
"features": requires.get("features", []),
|
|
62
|
+
"optional": requires.get("features_optional", []),
|
|
63
|
+
}
|
|
64
|
+
return None
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@click.command(
|
|
68
|
+
"check:contracts",
|
|
69
|
+
short_help="Check that feature contracts and SPL UVL constraints agree.",
|
|
70
|
+
)
|
|
71
|
+
@click.option("--spl", "spl_name", default=None, help="Check a single SPL.")
|
|
72
|
+
def check_contracts(spl_name):
|
|
73
|
+
"""
|
|
74
|
+
Cross-check requires.features in every contract against the UVL
|
|
75
|
+
constraints of each SPL in splent_catalog.
|
|
76
|
+
|
|
77
|
+
\b
|
|
78
|
+
Examples:
|
|
79
|
+
splent check:contracts
|
|
80
|
+
splent check:contracts --spl cms_spl
|
|
81
|
+
"""
|
|
82
|
+
workspace = str(context.workspace())
|
|
83
|
+
catalog = Path(workspace) / "splent_catalog"
|
|
84
|
+
|
|
85
|
+
if spl_name:
|
|
86
|
+
spl_dirs = [catalog / spl_name]
|
|
87
|
+
if not spl_dirs[0].is_dir():
|
|
88
|
+
click.secho(f"❌ SPL not found: {spl_dirs[0]}", fg="red")
|
|
89
|
+
raise SystemExit(1)
|
|
90
|
+
else:
|
|
91
|
+
spl_dirs = sorted(d for d in catalog.glob("*") if (d / "metadata.toml").is_file())
|
|
92
|
+
|
|
93
|
+
if not spl_dirs:
|
|
94
|
+
click.secho("❌ No SPLs found in splent_catalog.", fg="red")
|
|
95
|
+
raise SystemExit(1)
|
|
96
|
+
|
|
97
|
+
total_errors = 0
|
|
98
|
+
total_warnings = 0
|
|
99
|
+
|
|
100
|
+
for spl_dir in spl_dirs:
|
|
101
|
+
spl_errors = 0
|
|
102
|
+
spl_warnings = 0
|
|
103
|
+
uvl_path = spl_dir / f"{spl_dir.name}.uvl"
|
|
104
|
+
click.echo()
|
|
105
|
+
click.echo(click.style(f" check:contracts — {spl_dir.name}", bold=True))
|
|
106
|
+
click.echo(click.style(f" {'─' * 60}", fg="bright_black"))
|
|
107
|
+
|
|
108
|
+
if not uvl_path.is_file():
|
|
109
|
+
click.secho(
|
|
110
|
+
f" ⚠ UVL not on disk ({uvl_path.name}) — run: splent spl:fetch "
|
|
111
|
+
f"{spl_dir.name}",
|
|
112
|
+
fg="yellow",
|
|
113
|
+
)
|
|
114
|
+
total_warnings += 1
|
|
115
|
+
continue
|
|
116
|
+
|
|
117
|
+
model = marketplace.parse_uvl_structure(uvl_path.read_text())
|
|
118
|
+
shorts = model["features"] # short -> {package, org, ...}
|
|
119
|
+
implications = {(a, b) for a, b in model["constraints"]}
|
|
120
|
+
|
|
121
|
+
contracts: dict[str, list[str] | None] = {}
|
|
122
|
+
for short, meta in shorts.items():
|
|
123
|
+
contracts[short] = _local_contract_requires(workspace, meta["package"])
|
|
124
|
+
|
|
125
|
+
missing_local = sorted(s for s, req in contracts.items() if req is None)
|
|
126
|
+
|
|
127
|
+
# 1. Contract deps that the UVL does not enforce.
|
|
128
|
+
for short, req in sorted(contracts.items()):
|
|
129
|
+
for dep in (req or {}).get("features", []):
|
|
130
|
+
if dep not in shorts:
|
|
131
|
+
# Dependency on a feature outside this SPL: the SPL simply
|
|
132
|
+
# cannot offer this feature safely without it.
|
|
133
|
+
click.secho(
|
|
134
|
+
f" ✗ {short} requires '{dep}' (contract), but '{dep}' "
|
|
135
|
+
f"is not a feature of {spl_dir.name}",
|
|
136
|
+
fg="red",
|
|
137
|
+
)
|
|
138
|
+
spl_errors += 1
|
|
139
|
+
elif (short, dep) not in implications:
|
|
140
|
+
click.secho(
|
|
141
|
+
f" ✗ {short} requires '{dep}' (contract), but the UVL "
|
|
142
|
+
f"has no '{short} => {dep}' constraint",
|
|
143
|
+
fg="red",
|
|
144
|
+
)
|
|
145
|
+
click.echo(
|
|
146
|
+
click.style(
|
|
147
|
+
f" fix: add '{short} => {dep}' to the UVL "
|
|
148
|
+
f"constraints (splent spl:add-constraints "
|
|
149
|
+
f"{spl_dir.name} is interactive)",
|
|
150
|
+
fg="bright_black",
|
|
151
|
+
)
|
|
152
|
+
)
|
|
153
|
+
spl_errors += 1
|
|
154
|
+
|
|
155
|
+
# 2. UVL implications with no contract behind them.
|
|
156
|
+
for a, b in sorted(implications):
|
|
157
|
+
req = contracts.get(a)
|
|
158
|
+
if a not in shorts or b not in shorts:
|
|
159
|
+
continue # constraint over infra features (redis, nginx…)
|
|
160
|
+
if req is None:
|
|
161
|
+
continue # feature not local — cannot verify
|
|
162
|
+
if b in req["optional"]:
|
|
163
|
+
continue # SPL hardening a soft (graceful) dependency — fine
|
|
164
|
+
if b not in req["features"]:
|
|
165
|
+
click.secho(
|
|
166
|
+
f" ⚠ UVL says '{a} => {b}' but {a}'s contract does not "
|
|
167
|
+
f"require '{b}'",
|
|
168
|
+
fg="yellow",
|
|
169
|
+
)
|
|
170
|
+
click.echo(
|
|
171
|
+
click.style(
|
|
172
|
+
f" fix: add \"{b}\" to requires.features_manual in "
|
|
173
|
+
f"{shorts[a]['package']}/pyproject.toml (preserved on "
|
|
174
|
+
"regeneration), or confirm it is SPL-level policy",
|
|
175
|
+
fg="bright_black",
|
|
176
|
+
)
|
|
177
|
+
)
|
|
178
|
+
spl_warnings += 1
|
|
179
|
+
|
|
180
|
+
if missing_local:
|
|
181
|
+
click.echo(
|
|
182
|
+
click.style(
|
|
183
|
+
f" ℹ not local (skipped): {', '.join(missing_local)}",
|
|
184
|
+
fg="bright_black",
|
|
185
|
+
)
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
if not spl_errors and not spl_warnings:
|
|
189
|
+
click.secho(" ✅ Contracts and UVL agree.", fg="green")
|
|
190
|
+
total_errors += spl_errors
|
|
191
|
+
total_warnings += spl_warnings
|
|
192
|
+
|
|
193
|
+
click.echo()
|
|
194
|
+
if total_errors or total_warnings:
|
|
195
|
+
click.echo(
|
|
196
|
+
click.style(
|
|
197
|
+
f" {total_errors} error(s), {total_warnings} warning(s).",
|
|
198
|
+
bold=True,
|
|
199
|
+
)
|
|
200
|
+
)
|
|
201
|
+
click.echo()
|
|
202
|
+
if total_errors:
|
|
203
|
+
raise SystemExit(1)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
cli_command = check_contracts
|
|
@@ -12,20 +12,16 @@ from pathlib import Path
|
|
|
12
12
|
import click
|
|
13
13
|
|
|
14
14
|
from splent_cli.services import context
|
|
15
|
+
from splent_cli.utils.archetype import ( # noqa: F401 (re-exported for callers)
|
|
16
|
+
ARCHETYPES,
|
|
17
|
+
ARCHETYPE_LABELS,
|
|
18
|
+
detect_archetype as _detect_archetype,
|
|
19
|
+
)
|
|
15
20
|
from splent_cli.utils.feature_utils import normalize_namespace
|
|
16
21
|
|
|
17
22
|
|
|
18
23
|
DEFAULT_NAMESPACE = os.getenv("SPLENT_DEFAULT_NAMESPACE", "splent_io")
|
|
19
24
|
|
|
20
|
-
ARCHETYPES = ("full", "light", "config", "service")
|
|
21
|
-
|
|
22
|
-
ARCHETYPE_LABELS = {
|
|
23
|
-
"full": "Complete domain feature (models, routes, services, templates, migrations)",
|
|
24
|
-
"light": "Lightweight UI feature (routes, hooks, templates)",
|
|
25
|
-
"config": "Infrastructure feature (config.py only)",
|
|
26
|
-
"service": "Backend service (services, config, commands, signals)",
|
|
27
|
-
}
|
|
28
|
-
|
|
29
25
|
# Files that are ALWAYS kept regardless of archetype
|
|
30
26
|
PROTECTED = {"__init__.py"}
|
|
31
27
|
|
|
@@ -115,32 +111,6 @@ def _is_stub_dir(dirpath, dirname):
|
|
|
115
111
|
return False
|
|
116
112
|
|
|
117
113
|
|
|
118
|
-
# ── Archetype detection ──────────────────────────────────────────────
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
def _detect_archetype(src_dir):
|
|
122
|
-
"""Detect the real archetype from source file contents."""
|
|
123
|
-
models = _read(os.path.join(src_dir, "models.py"))
|
|
124
|
-
routes = _read(os.path.join(src_dir, "routes.py"))
|
|
125
|
-
services = _read(os.path.join(src_dir, "services.py"))
|
|
126
|
-
|
|
127
|
-
has_models = models and re.search(r"db\.Column", models)
|
|
128
|
-
has_routes = routes and re.search(r"@\w+\.route\s*\(", routes)
|
|
129
|
-
has_services = services and re.search(
|
|
130
|
-
r"def\s+(?!__)\w+\s*\(.*\):\s*\n\s+(?!pass\b|\.\.\.|\s*#)",
|
|
131
|
-
services,
|
|
132
|
-
re.MULTILINE,
|
|
133
|
-
)
|
|
134
|
-
|
|
135
|
-
if has_models:
|
|
136
|
-
return "full"
|
|
137
|
-
if has_routes:
|
|
138
|
-
return "light"
|
|
139
|
-
if has_services:
|
|
140
|
-
return "service"
|
|
141
|
-
return "config"
|
|
142
|
-
|
|
143
|
-
|
|
144
114
|
# ── Expected files per archetype ─────────────────────────────────────
|
|
145
115
|
|
|
146
116
|
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import os
|
|
2
2
|
import re
|
|
3
3
|
import shutil
|
|
4
|
-
import requests
|
|
5
4
|
import click
|
|
6
|
-
from splent_cli.services import context
|
|
5
|
+
from splent_cli.services import context, registry
|
|
7
6
|
from splent_cli.utils.cache_utils import make_feature_readonly
|
|
8
7
|
from splent_cli.utils.feature_utils import normalize_namespace
|
|
9
8
|
from splent_cli.utils.proc import require_tool
|
|
@@ -17,38 +16,6 @@ DEFAULT_NAMESPACE = os.getenv("SPLENT_DEFAULT_NAMESPACE", "splent_io")
|
|
|
17
16
|
# =====================================================================
|
|
18
17
|
|
|
19
18
|
|
|
20
|
-
def _get_latest_tag(namespace, repo) -> str | None:
|
|
21
|
-
"""Fetch the highest semver tag from GitHub. Returns None if unreachable or no tags."""
|
|
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}"
|
|
30
|
-
try:
|
|
31
|
-
r = requests.get(api_url, headers=headers, timeout=5)
|
|
32
|
-
r.raise_for_status()
|
|
33
|
-
tags = r.json()
|
|
34
|
-
if not tags:
|
|
35
|
-
return None
|
|
36
|
-
versions = []
|
|
37
|
-
for tag in tags:
|
|
38
|
-
name = tag.get("name", "")
|
|
39
|
-
m = re.match(r"v?(\d+)\.(\d+)\.(\d+)", name)
|
|
40
|
-
if m:
|
|
41
|
-
versions.append(
|
|
42
|
-
(int(m.group(1)), int(m.group(2)), int(m.group(3)), name)
|
|
43
|
-
)
|
|
44
|
-
if not versions:
|
|
45
|
-
return tags[0]["name"]
|
|
46
|
-
versions.sort(reverse=True)
|
|
47
|
-
return versions[0][3]
|
|
48
|
-
except (requests.RequestException, KeyError, IndexError, ValueError):
|
|
49
|
-
return None
|
|
50
|
-
|
|
51
|
-
|
|
52
19
|
def _parse_full_name(full_name: str):
|
|
53
20
|
"""
|
|
54
21
|
full_name = <namespace>/<repo>[@version]
|
|
@@ -104,7 +71,7 @@ def feature_clone(full_name):
|
|
|
104
71
|
click.echo(
|
|
105
72
|
f"🔍 No version provided → fetching latest tag for {namespace}/{repo}..."
|
|
106
73
|
)
|
|
107
|
-
version =
|
|
74
|
+
version = registry.latest_semver_tag(namespace, repo)
|
|
108
75
|
if not version:
|
|
109
76
|
click.secho(
|
|
110
77
|
f"❌ Could not fetch tags for {namespace}/{repo}. Is the repo reachable and does it have tags?",
|
|
@@ -90,6 +90,9 @@ def _read_current_contract(pyproject_path: Path) -> dict:
|
|
|
90
90
|
raw = data.get("tool", {}).get("splent", {}).get("contract", {})
|
|
91
91
|
ext = raw.get("extensible", {})
|
|
92
92
|
return {
|
|
93
|
+
"archetype": raw.get("archetype"),
|
|
94
|
+
"category": raw.get("category"),
|
|
95
|
+
"tags": raw.get("tags", []),
|
|
93
96
|
"routes": raw.get("provides", {}).get("routes", []),
|
|
94
97
|
"blueprints": raw.get("provides", {}).get("blueprints", []),
|
|
95
98
|
"models": raw.get("provides", {}).get("models", []),
|
|
@@ -100,6 +103,12 @@ def _read_current_contract(pyproject_path: Path) -> dict:
|
|
|
100
103
|
"translations": raw.get("provides", {}).get("translations", []),
|
|
101
104
|
"docker": raw.get("provides", {}).get("docker", []),
|
|
102
105
|
"requires_features": raw.get("requires", {}).get("features", []),
|
|
106
|
+
"requires_features_manual": raw.get("requires", {}).get(
|
|
107
|
+
"features_manual", []
|
|
108
|
+
),
|
|
109
|
+
"requires_features_optional": raw.get("requires", {}).get(
|
|
110
|
+
"features_optional", []
|
|
111
|
+
),
|
|
103
112
|
"env_vars": raw.get("requires", {}).get("env_vars", []),
|
|
104
113
|
"requires_signals": raw.get("requires", {}).get("signals", []),
|
|
105
114
|
"extensible_services": ext.get("services", []),
|
|
@@ -135,6 +144,10 @@ def _print_contract(contract: dict, feature_name: str) -> None:
|
|
|
135
144
|
return "[" + ", ".join(f'"{i}"' for i in items) + "]"
|
|
136
145
|
|
|
137
146
|
click.echo()
|
|
147
|
+
if contract.get("archetype"):
|
|
148
|
+
click.echo(click.style(" [tool.splent.contract]", fg="bright_black"))
|
|
149
|
+
click.echo(f" archetype = \"{contract['archetype']}\"")
|
|
150
|
+
click.echo()
|
|
138
151
|
click.echo(click.style(" [tool.splent.contract.provides]", fg="bright_black"))
|
|
139
152
|
click.echo(f" routes = {_fmt(contract['routes'])}")
|
|
140
153
|
click.echo(f" blueprints = {_fmt(contract['blueprints'])}")
|
|
@@ -148,6 +161,9 @@ def _print_contract(contract: dict, feature_name: str) -> None:
|
|
|
148
161
|
click.echo()
|
|
149
162
|
click.echo(click.style(" [tool.splent.contract.requires]", fg="bright_black"))
|
|
150
163
|
click.echo(f" features = {_fmt(contract['requires_features'])}")
|
|
164
|
+
click.echo(
|
|
165
|
+
f" features_optional = {_fmt(contract.get('requires_features_optional', []))}"
|
|
166
|
+
)
|
|
151
167
|
click.echo(f" env_vars = {_fmt(contract['env_vars'])}")
|
|
152
168
|
click.echo(f" signals = {_fmt(contract.get('requires_signals', []))}")
|
|
153
169
|
click.echo()
|
|
@@ -190,6 +206,7 @@ def _print_diff(current: dict, inferred: dict) -> bool:
|
|
|
190
206
|
("services", "services"),
|
|
191
207
|
("docker", "docker"),
|
|
192
208
|
("requires_features", "requires.features"),
|
|
209
|
+
("requires_features_optional", "requires.features_optional"),
|
|
193
210
|
("env_vars", "requires.env_vars"),
|
|
194
211
|
("signals", "provides.signals"),
|
|
195
212
|
("translations", "provides.translations"),
|
|
@@ -205,6 +222,17 @@ def _print_diff(current: dict, inferred: dict) -> bool:
|
|
|
205
222
|
]
|
|
206
223
|
|
|
207
224
|
diff_lines = []
|
|
225
|
+
|
|
226
|
+
# archetype is a scalar, not a list — compare it directly
|
|
227
|
+
old_arch = current.get("archetype")
|
|
228
|
+
new_arch = inferred.get("archetype")
|
|
229
|
+
if new_arch and old_arch != new_arch:
|
|
230
|
+
if old_arch:
|
|
231
|
+
diff_lines.append(
|
|
232
|
+
click.style(f" - archetype: {old_arch}", fg="red")
|
|
233
|
+
)
|
|
234
|
+
diff_lines.append(click.style(f" + archetype: {new_arch}", fg="green"))
|
|
235
|
+
|
|
208
236
|
for key, label in FIELDS:
|
|
209
237
|
diff_lines.extend(
|
|
210
238
|
_diff_field(label, current.get(key, []), inferred.get(key, []))
|
|
@@ -268,6 +296,22 @@ def feature_contract(feature_ref, write):
|
|
|
268
296
|
click.echo(" 🔍 Scanning source code…")
|
|
269
297
|
inferred = infer_contract(str(cache_path), ns, name)
|
|
270
298
|
|
|
299
|
+
# Deps declared in requires.features_manual are preserved by
|
|
300
|
+
# write_contract; merge them here too so the dry-run diff shows exactly
|
|
301
|
+
# what --write would produce (including real drops of stale inferred deps).
|
|
302
|
+
current = _read_current_contract(pyproject_path)
|
|
303
|
+
manual = sorted(set(current.get("requires_features_manual", [])))
|
|
304
|
+
if manual:
|
|
305
|
+
inferred["requires_features"] = sorted(
|
|
306
|
+
set(inferred["requires_features"]) | set(manual)
|
|
307
|
+
)
|
|
308
|
+
click.echo(
|
|
309
|
+
click.style(
|
|
310
|
+
" Preserving requires.features_manual: ", fg="bright_black"
|
|
311
|
+
)
|
|
312
|
+
+ ", ".join(manual)
|
|
313
|
+
)
|
|
314
|
+
|
|
271
315
|
# Show inferred contract
|
|
272
316
|
click.echo(click.style(" Inferred contract:", bold=True))
|
|
273
317
|
_print_contract(inferred, name)
|
|
@@ -280,7 +324,6 @@ def feature_contract(feature_ref, write):
|
|
|
280
324
|
inferred["docker_networks"] = dc.get("networks", [])
|
|
281
325
|
|
|
282
326
|
# Compare with current
|
|
283
|
-
current = _read_current_contract(pyproject_path)
|
|
284
327
|
has_changes = _print_diff(current, inferred)
|
|
285
328
|
|
|
286
329
|
if not has_changes:
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
"""
|
|
2
|
+
feature:info — Show a feature's contract card without cloning anything.
|
|
3
|
+
|
|
4
|
+
Data comes from the marketplace index (built with `marketplace:index` or
|
|
5
|
+
fetched from SPLENT_INDEX_URL). When no index is available, falls back to
|
|
6
|
+
reading the feature's pyproject.toml straight from GitHub at its latest
|
|
7
|
+
released tag — always the real contract, never a hand-maintained copy.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import os
|
|
12
|
+
import tomllib
|
|
13
|
+
|
|
14
|
+
import click
|
|
15
|
+
|
|
16
|
+
from splent_cli.services import context, marketplace, registry
|
|
17
|
+
from splent_cli.utils.feature_utils import (
|
|
18
|
+
get_normalize_feature_name_in_splent_format,
|
|
19
|
+
load_product_features,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _fetch_live_entry(org: str, repo: str, token: str | None) -> dict | None:
|
|
24
|
+
"""Build an index-shaped entry for one feature straight from GitHub."""
|
|
25
|
+
version = registry.latest_semver_tag(org, repo, token)
|
|
26
|
+
if not version:
|
|
27
|
+
return None
|
|
28
|
+
text = registry.fetch_file(org, repo, "pyproject.toml", ref=version, token=token)
|
|
29
|
+
if text is None:
|
|
30
|
+
return None
|
|
31
|
+
try:
|
|
32
|
+
return marketplace.feature_entry_from_pyproject(
|
|
33
|
+
text, org=org, repo=repo, source="github", version=version
|
|
34
|
+
)
|
|
35
|
+
except tomllib.TOMLDecodeError:
|
|
36
|
+
return None
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _product_feature_shorts() -> set[str]:
|
|
40
|
+
"""Short names of the features in the active product ('' set when detached)."""
|
|
41
|
+
product = context.active_app()
|
|
42
|
+
if not product:
|
|
43
|
+
return set()
|
|
44
|
+
try:
|
|
45
|
+
entries = load_product_features(
|
|
46
|
+
os.path.join(str(context.workspace()), product), os.getenv("SPLENT_ENV")
|
|
47
|
+
)
|
|
48
|
+
except (FileNotFoundError, SystemExit):
|
|
49
|
+
return set()
|
|
50
|
+
return {
|
|
51
|
+
e.split("@")[0].split("/")[-1].removeprefix("splent_feature_")
|
|
52
|
+
for e in entries
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _print_list(label: str, items: list, color: str | None = None) -> None:
|
|
57
|
+
if not items:
|
|
58
|
+
return
|
|
59
|
+
click.echo(f" {label:<14}" + click.style(", ".join(items), fg=color))
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@click.command(
|
|
63
|
+
"feature:info",
|
|
64
|
+
short_help="Show a feature's contract, dependencies and usage without cloning it.",
|
|
65
|
+
)
|
|
66
|
+
@click.argument("feature_ref")
|
|
67
|
+
@click.option("--refresh", is_flag=True, help="Re-fetch the index from SPLENT_INDEX_URL.")
|
|
68
|
+
@click.option("--json", "as_json", is_flag=True, help="Print the raw entry as JSON.")
|
|
69
|
+
def feature_info(feature_ref, refresh, as_json):
|
|
70
|
+
"""
|
|
71
|
+
Show everything the marketplace knows about a feature.
|
|
72
|
+
|
|
73
|
+
\b
|
|
74
|
+
Examples:
|
|
75
|
+
splent feature:info auth
|
|
76
|
+
splent feature:info splent-io/splent_feature_events
|
|
77
|
+
splent feature:info media --json
|
|
78
|
+
"""
|
|
79
|
+
workspace = str(context.workspace())
|
|
80
|
+
token = registry.github_token()
|
|
81
|
+
|
|
82
|
+
index, origin = marketplace.resolve_index(workspace, refresh=refresh)
|
|
83
|
+
|
|
84
|
+
entry = None
|
|
85
|
+
if index:
|
|
86
|
+
entry = marketplace.find_feature(index, feature_ref)
|
|
87
|
+
|
|
88
|
+
if entry is None:
|
|
89
|
+
# Live fallback: one feature, straight from GitHub.
|
|
90
|
+
if "/" in feature_ref:
|
|
91
|
+
org, _, repo = feature_ref.partition("/")
|
|
92
|
+
else:
|
|
93
|
+
org = os.getenv("SPLENT_DEFAULT_ORG", "splent-io")
|
|
94
|
+
repo = feature_ref
|
|
95
|
+
repo = get_normalize_feature_name_in_splent_format(repo.split("@")[0])
|
|
96
|
+
entry = _fetch_live_entry(org, repo, token)
|
|
97
|
+
origin = "github (live)"
|
|
98
|
+
|
|
99
|
+
if entry is None:
|
|
100
|
+
click.secho(f"❌ Feature '{feature_ref}' not found.", fg="red")
|
|
101
|
+
click.secho(
|
|
102
|
+
" Build an index with: splent marketplace:index "
|
|
103
|
+
"(or check the name/org and your network).",
|
|
104
|
+
fg="yellow",
|
|
105
|
+
)
|
|
106
|
+
raise SystemExit(1)
|
|
107
|
+
|
|
108
|
+
if as_json:
|
|
109
|
+
click.echo(json.dumps(entry, indent=2, ensure_ascii=False))
|
|
110
|
+
return
|
|
111
|
+
|
|
112
|
+
installed = _product_feature_shorts()
|
|
113
|
+
provides = entry.get("provides", {})
|
|
114
|
+
requires = entry.get("requires", {})
|
|
115
|
+
|
|
116
|
+
click.echo()
|
|
117
|
+
header = f" {entry['id']}"
|
|
118
|
+
if entry.get("version"):
|
|
119
|
+
header += click.style(f" {entry['version']}", fg="green")
|
|
120
|
+
if entry.get("archetype"):
|
|
121
|
+
header += click.style(f" [{entry['archetype']}]", fg="cyan")
|
|
122
|
+
if entry["short"] in installed:
|
|
123
|
+
header += click.style(" ● installed", fg="green")
|
|
124
|
+
click.echo(click.style(header, bold=True))
|
|
125
|
+
click.echo(click.style(f" {'─' * 60}", fg="bright_black"))
|
|
126
|
+
|
|
127
|
+
if entry.get("description"):
|
|
128
|
+
click.echo(f" {entry['description']}")
|
|
129
|
+
click.echo()
|
|
130
|
+
|
|
131
|
+
if entry.get("category"):
|
|
132
|
+
click.echo(f" {'category':<14}{entry['category']}")
|
|
133
|
+
_print_list("tags", entry.get("tags", []))
|
|
134
|
+
if entry.get("env"):
|
|
135
|
+
click.echo(
|
|
136
|
+
f" {'env':<14}" + click.style(f"{entry['env']} only", fg="yellow")
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
_print_list("models", provides.get("models", []))
|
|
140
|
+
_print_list("services", provides.get("services", []))
|
|
141
|
+
_print_list("routes", provides.get("routes", []))
|
|
142
|
+
_print_list("hooks", provides.get("hooks", []))
|
|
143
|
+
_print_list("signals", provides.get("signals", []))
|
|
144
|
+
_print_list("commands", provides.get("commands", []))
|
|
145
|
+
_print_list("translations", provides.get("translations", []))
|
|
146
|
+
if entry.get("docker"):
|
|
147
|
+
_print_list("docker", entry["docker"].get("services", []), color="cyan")
|
|
148
|
+
|
|
149
|
+
req_features = requires.get("features", [])
|
|
150
|
+
if req_features:
|
|
151
|
+
click.echo()
|
|
152
|
+
decorated = []
|
|
153
|
+
for dep in req_features:
|
|
154
|
+
if dep in installed:
|
|
155
|
+
decorated.append(dep + click.style(" ✔", fg="green"))
|
|
156
|
+
else:
|
|
157
|
+
decorated.append(dep + click.style(" ✗ missing", fg="yellow"))
|
|
158
|
+
click.echo(f" {'requires':<14}" + ", ".join(decorated))
|
|
159
|
+
_print_list("env vars", requires.get("env_vars", []), color="bright_black")
|
|
160
|
+
_print_list("needs signals", requires.get("signals", []), color="bright_black")
|
|
161
|
+
|
|
162
|
+
if index:
|
|
163
|
+
closure = marketplace.dependency_closure(index, entry["short"])
|
|
164
|
+
extra = sorted(set(closure) - set(req_features))
|
|
165
|
+
if extra:
|
|
166
|
+
_print_list("transitively", extra, color="bright_black")
|
|
167
|
+
|
|
168
|
+
if entry.get("used_by"):
|
|
169
|
+
click.echo()
|
|
170
|
+
_print_list("used by", entry["used_by"], color="cyan")
|
|
171
|
+
|
|
172
|
+
if entry.get("refinement"):
|
|
173
|
+
refines = entry["refinement"].get("refines")
|
|
174
|
+
if refines:
|
|
175
|
+
click.echo()
|
|
176
|
+
click.echo(
|
|
177
|
+
f" {'refines':<14}" + click.style(refines, fg="magenta")
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
pypi = entry.get("pypi")
|
|
181
|
+
if pypi is not None:
|
|
182
|
+
click.echo()
|
|
183
|
+
if pypi.get("published"):
|
|
184
|
+
label = f"published (latest {pypi.get('latest')})"
|
|
185
|
+
color = "green" if pypi.get("has_current") else "yellow"
|
|
186
|
+
if not pypi.get("has_current"):
|
|
187
|
+
label += " — current tag not on PyPI yet"
|
|
188
|
+
click.echo(f" {'pypi':<14}" + click.style(label, fg=color))
|
|
189
|
+
else:
|
|
190
|
+
click.echo(
|
|
191
|
+
f" {'pypi':<14}"
|
|
192
|
+
+ click.style("not published (dev-only via GitHub)", fg="yellow")
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
gh = entry.get("github") or {}
|
|
196
|
+
if gh.get("url"):
|
|
197
|
+
click.echo(f" {'repo':<14}{gh['url']}")
|
|
198
|
+
|
|
199
|
+
click.echo()
|
|
200
|
+
click.echo(
|
|
201
|
+
click.style(f" source: {origin or entry.get('source', '?')}", fg="bright_black")
|
|
202
|
+
)
|
|
203
|
+
if entry["short"] not in installed and context.active_app():
|
|
204
|
+
click.echo()
|
|
205
|
+
click.echo(
|
|
206
|
+
" install with: "
|
|
207
|
+
+ click.style(
|
|
208
|
+
f"splent feature:install {entry['org']}/{entry['repo']}", bold=True
|
|
209
|
+
)
|
|
210
|
+
)
|
|
211
|
+
click.echo()
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
cli_command = feature_info
|