diff --git a/.rapidkit/release/promote-manifest.json b/.rapidkit/release/promote-manifest.json index 4d96b0b..0799792 100644 --- a/.rapidkit/release/promote-manifest.json +++ b/.rapidkit/release/promote-manifest.json @@ -1,103 +1,23 @@ { "excluded_commits": [ - { - "reason": "protected workflow-only commit", - "sha": "968ac3613a89e9c9d94cde9789c989e56f21979d", - "subject": "fix: validate staging promote token before tagging" - }, - { - "reason": "protected workflow-only commit", - "sha": "54d03752f2a08fab756099f91ff68dee3e56cf16", - "subject": "fix(ci): pass SOURCE_REF env to curated promote step" - }, - { - "reason": "protected workflow-only commit", - "sha": "8b7c59fa2477b9dc56900116d806b9c22b3ea247", - "subject": "feat(ci): optional publish input for RC tag; pass SOURCE_REF" - }, - { - "reason": "protected workflow-only commit", - "sha": "a9f3b6e7dfb6ee744e4345aa2a5fd41509e6ed20", - "subject": "fix(release): exclude protected paths at patch generation" - }, - { - "reason": "protected workflow-only commit", - "sha": "7097d0e01290f438e98f23c84168af5da5bc63f1", - "subject": "fix(ci): always use promote tooling from main when promoting a tag" - }, - { - "reason": "protected workflow-only commit", - "sha": "0035d1c252482c6058e3a09e8aeb6729011a3669", - "subject": "fix(release): skip engine-only modified/deleted paths in curated promote" - }, - { - "reason": "protected workflow-only commit", - "sha": "3c00d4e007b4fdd9b0a6ae9be4a758e71b068d28", - "subject": "fix(release): exclude tier-owned files from promote" - }, - { - "reason": "protected workflow-only commit", - "sha": "6b6e6840642ba76787fd23a0629cb9d1c7436565", - "subject": "fix(release): force-add promote manifest past target gitignore" - }, { "reason": "empty after protected-path restore", - "sha": "fe17f029b51f4b1b97d41b3afcb6e021ee32b9e0", - "subject": "feat(release): redact engine repo and auto-snapshot changelog" + "sha": "306d5b8bccee82f33a988fb34bb5606e538592eb", + "subject": "feat(cli): add module identity util and harden module lifecycle commands" } ], "included_commits": [ { - "sha": "1e1613e1c7f29e4ad3c28965afa21106186746cf", - "subject": "fix: enforce module-owned generated paths" - }, - { - "sha": "0c4b3ac52ab3c1322aacc9ead1de261d5188e7e3", - "subject": "fix: align module test artifacts with owned paths" - }, - { - "sha": "f0db77ae0186a62c9431ec0f74b7be7a4e20173c", - "subject": "fix: curate staging promotion into public core" - }, - { - "sha": "8245c11601b35d5274f76f3711a65441fa150b4d", - "subject": "chore: align module docs domain" - }, - { - "sha": "bf5b2aad605f380074a22b58353cb4ea4b228531", - "subject": "chore: sync module metadata after docs domain update" - }, - { - "sha": "e18b04679f64a3bff79148de76b2642b91493cca", - "subject": "chore: investigate free/ai/rag_pipeline stabilization (logs + tests)" - }, - { - "sha": "23ecd477c4062128de6878e4d5fa8c12924b3622", - "subject": "chore: investigate free/ai/rag_pipeline stabilization (logs + tests)" - }, - { - "sha": "d8ec650301393c4c04c0c2cae90d94c3a5ed07c5", - "subject": "chore: add generator contract tests and module metadata updates" - }, - { - "sha": "16a23751d439bcdaef2409550185b4402d3292d0", - "subject": "chore: add generator contract tests and module metadata updates" - }, - { - "sha": "bbe759dc17565ab39fd50be233a3564c5e639bc0", - "subject": "fix(release): persist promote manifest and harden stable tag" - }, - { - "sha": "56e6b195fdf7644e982749e9127891d0b07757eb", - "subject": "fix(release): anchor promote chain to staging tags and source.json" + "sha": "83fedbfd4236f8ecf8407541b810dabd7ec04a4f", + "subject": "feat(cli): add module identity util and harden module lifecycle commands" } ], - "promote_branch": "promote/v0.5.3", - "promoted_at": "2026-06-10T23:35:59.946914Z", + "promote_branch": "promote/v0.5.4", + "promoted_at": "2026-06-11T22:02:02.636849Z", "schema_version": 1, - "source_base_sha": "2529eeabd3aeff807c2148cc7d60f9f2d94f206c", - "source_head_sha": "fe17f029b51f4b1b97d41b3afcb6e021ee32b9e0", + "source_base_sha": "fe17f029b51f4b1b97d41b3afcb6e021ee32b9e0", + "source_head_sha": "306d5b8bccee82f33a988fb34bb5606e538592eb", "target_branch": "main", "target_repo": "https://github.com/rapidkitlabs/rapidkit-core.git", - "version": "0.5.3" + "version": "0.5.4" } diff --git a/CHANGELOG.md b/CHANGELOG.md index 1d94413..4d44494 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ +## [Snapshot 0.5.4] - 2026-06-11 + +> Auto-generated promote snapshot for this version. + +### Promoted source commits + +- feat(cli): add module identity util and harden module lifecycle commands (`83fedbfd4236`) + ## [Snapshot 0.5.3] - 2026-06-10 > Auto-generated promote snapshot for this version. diff --git a/pyproject.toml b/pyproject.toml index 93158d7..7552047 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "rapidkit-core" -version = "0.5.3" +version = "0.5.4" description = "Community-built rapid automation toolkit for makers who ship with heart" readme = "README.md" requires-python = ">=3.10,<4.0" @@ -73,7 +73,7 @@ Documentation = "https://www.getrapidkit.com/docs" [tool.poetry] name = "rapidkit-core" -version = "0.5.3" +version = "0.5.4" description = "Community-built rapid automation toolkit for makers who ship with heart" authors = ["Baziar "] readme = "README.md" diff --git a/src/cli/commands/checkpoint.py b/src/cli/commands/checkpoint.py index c9e567e..bddaca9 100644 --- a/src/cli/commands/checkpoint.py +++ b/src/cli/commands/checkpoint.py @@ -17,6 +17,7 @@ from ..ui.printer import print_error, print_info, print_success, print_warning from ..utils.filesystem import find_project_root +from ..utils.module_identity import module_identity_matches checkpoint_app = typer.Typer(help="Create rollback checkpoints for a module's files") @@ -55,7 +56,11 @@ def checkpoint_module( manifest = load_manifest_or_none(MODULES_PATH, name) registry = load_hashes(project_root) files_meta: Dict[str, Dict[str, Any]] = registry.get("files", {}) - targets = {p: meta for p, meta in files_meta.items() if meta.get("module") == name} + targets = { + p: meta + for p, meta in files_meta.items() + if module_identity_matches(meta.get("module"), name) + } updated = [] skipped = [] diff --git a/src/cli/commands/modules.py b/src/cli/commands/modules.py index 3a84c53..4bf3dc7 100644 --- a/src/cli/commands/modules.py +++ b/src/cli/commands/modules.py @@ -32,6 +32,7 @@ from ..ui.printer import console, print_error, print_info, print_success, print_warning from ..utils.filesystem import find_project_root +from ..utils.module_identity import module_identity_matches from ..utils.module_structure_cli import ( DEFAULT_MODULES_ROOT, collect_validation_results, @@ -233,14 +234,10 @@ def _discover_free_manifest_paths(modules_root: Path) -> List[Path]: def _module_matches_filter(recorded: Optional[str], requested: Optional[str]) -> bool: if not requested: return True - if not recorded: - return False requested_norm = requested.strip() if not requested_norm: return True - if recorded == requested_norm: - return True - return recorded.endswith(f"/{requested_norm}") + return module_identity_matches(recorded, requested_norm) def _registry_lookup_by_slug(registry: Any, slug: str) -> Optional[Dict[str, Any]]: diff --git a/src/cli/commands/rollback.py b/src/cli/commands/rollback.py index bfe0255..ccbee9a 100644 --- a/src/cli/commands/rollback.py +++ b/src/cli/commands/rollback.py @@ -19,6 +19,7 @@ from ..ui.printer import print_error, print_info, print_success, print_warning from ..utils.filesystem import find_project_root +from ..utils.module_identity import module_identity_matches rollback_app = typer.Typer(help="Rollback generated files to previous_hash snapshot") @@ -182,7 +183,11 @@ def rollback_module( template_lookup[str(Path(root_path) / rel if root_path else Path(rel))] = tpl except (OSError, json.JSONDecodeError): template_lookup = None - targets = [p for p, meta in registry.get("files", {}).items() if meta.get("module") == name] + targets = [ + p + for p, meta in registry.get("files", {}).items() + if module_identity_matches(meta.get("module"), name) + ] results = [ _rollback_file( project_root, diff --git a/src/cli/commands/snapshot.py b/src/cli/commands/snapshot.py index f015d04..8d5efa4 100644 --- a/src/cli/commands/snapshot.py +++ b/src/cli/commands/snapshot.py @@ -18,6 +18,7 @@ from ..ui.printer import print_error, print_info, print_success, print_warning from ..utils.filesystem import find_project_root +from ..utils.module_identity import module_identity_matches snapshot_app = typer.Typer(help="Snapshot utilities (backfill missing snapshots)") @@ -97,7 +98,11 @@ def snapshot_backfill( registry = load_hashes(project_root) files_meta: Dict[str, Dict[str, Any]] = registry.get("files", {}) targets = ( - {p: meta for p, meta in files_meta.items() if meta.get("module") == module} + { + p: meta + for p, meta in files_meta.items() + if module_identity_matches(meta.get("module"), module) + } if module else files_meta ) diff --git a/src/cli/commands/uninstall.py b/src/cli/commands/uninstall.py index 5c7543f..979f193 100644 --- a/src/cli/commands/uninstall.py +++ b/src/cli/commands/uninstall.py @@ -9,6 +9,8 @@ from ..ui.printer import print_error, print_info, print_success, print_warning from ..utils.filesystem import find_project_root +from ..utils.module_identity import module_identity_matches +from ..utils.registry import remove_from_registry uninstall_app = typer.Typer(help="Uninstall (remove) generated files of a module") @@ -41,7 +43,11 @@ def uninstall_module( } # ensure registry points to cleaned mapping (so removals persist) registry["files"] = files_meta - targets = {p: meta for p, meta in files_meta.items() if meta.get("module") == name} + targets = { + p: meta + for p, meta in files_meta.items() + if module_identity_matches(meta.get("module"), name) + } removed: List[str] = [] skipped: List[Dict[str, str]] = [] for rel, meta in targets.items(): @@ -72,6 +78,8 @@ def uninstall_module( if not dry_run: save_hashes(project_root, registry) + if removed: + remove_from_registry(name, project_root) summary = { "module": name, diff --git a/src/cli/utils/module_identity.py b/src/cli/utils/module_identity.py new file mode 100644 index 0000000..59ab58b --- /dev/null +++ b/src/cli/utils/module_identity.py @@ -0,0 +1,26 @@ +from __future__ import annotations + +from typing import Optional + + +def module_identity_matches(recorded: Optional[str], requested: Optional[str]) -> bool: + """Match hash-registry module labels against CLI module slugs. + + Hash registries may store short names (``agent_runtime``) while the CLI accepts + fully-qualified slugs (``free/ai/agent_runtime``). + """ + + if not recorded or not requested: + return False + + rec = str(recorded).strip().strip("/") + req = str(requested).strip().strip("/") + if not rec or not req: + return False + + if rec == req: + return True + if rec.endswith(f"/{req}") or req.endswith(f"/{rec}"): + return True + + return rec.split("/")[-1] == req.split("/")[-1] diff --git a/src/cli/utils/registry.py b/src/cli/utils/registry.py index 726425f..2fd9063 100644 --- a/src/cli/utils/registry.py +++ b/src/cli/utils/registry.py @@ -5,6 +5,7 @@ from typing import Any, Dict, List from ..ui.printer import print_success, print_warning +from .module_identity import module_identity_matches def _normalise_registry_entries(raw_entries: Any) -> List[Dict[str, Any]]: @@ -72,3 +73,29 @@ def update_registry( print_success(f"🔄 Refreshed registry metadata for module: {module_name}") except (OSError, PermissionError) as e: print_warning(f"⚠️ Could not update registry: {e}") + + +def remove_from_registry(module_name: str, project_root: Path) -> bool: + """Remove a module entry from registry.json when its slug matches.""" + + registry_path = project_root / "registry.json" + if not registry_path.exists(): + return False + + try: + data = json.loads(registry_path.read_text(encoding="utf-8")) + except json.JSONDecodeError: + return False + + entries = _normalise_registry_entries(data.get("installed_modules", [])) + filtered = [ + entry for entry in entries if not module_identity_matches(entry.get("slug"), module_name) + ] + + if len(filtered) == len(entries): + return False + + data["installed_modules"] = filtered + registry_path.write_text(json.dumps(data, indent=2), encoding="utf-8") + print_success(f"✅ Removed module from registry: {module_name}") + return True diff --git a/src/core/config/version.py b/src/core/config/version.py index 9d2c22b..567874b 100644 --- a/src/core/config/version.py +++ b/src/core/config/version.py @@ -6,7 +6,7 @@ from packaging import version as packaging_version -CURRENT_VERSION = "0.5.3" +CURRENT_VERSION = "0.5.4" def check_min_version(min_version: str, current_version: str) -> None: diff --git a/tests/cli/utils/test_module_identity.py b/tests/cli/utils/test_module_identity.py new file mode 100644 index 0000000..ef42102 --- /dev/null +++ b/tests/cli/utils/test_module_identity.py @@ -0,0 +1,20 @@ +import pytest + +from cli.utils.module_identity import module_identity_matches + + +@pytest.mark.parametrize( + ("recorded", "requested", "expected"), + [ + ("agent_runtime", "free/ai/agent_runtime", True), + ("llm_gateway", "free/ai/llm_gateway", True), + ("free/ai/agent_runtime", "free/ai/agent_runtime", True), + ("settings", "free/essentials/settings", True), + ("logging", "free/essentials/logging", True), + ("agent_runtime", "free/ai/llm_gateway", False), + (None, "free/ai/agent_runtime", False), + ("agent_runtime", "", False), + ], +) +def test_module_identity_matches(recorded, requested, expected) -> None: + assert module_identity_matches(recorded, requested) is expected