{"record":{"id":"ada3f49ab61a43e5","repo":"github/spec-kit","slug":"extension-extension-id-is-not-installed","errorCode":null,"errorMessage":"Extension '{extension_id}' is not installed","messagePattern":"Extension '(.+?)' is not installed","errorType":"exception","errorClass":"KeyError","httpStatus":null,"severity":"error","filePath":"src/specify_cli/extensions/__init__.py","lineNumber":855,"sourceCode":"\n        Merges the provided metadata with the existing entry, preserving any\n        fields not specified in the new metadata. The installed_at timestamp\n        is always preserved from the original entry.\n\n        Use this method instead of add() when updating existing extension\n        metadata (e.g., enabling/disabling) to preserve the original\n        installation timestamp and other existing fields.\n\n        Args:\n            extension_id: Extension ID\n            metadata: Extension metadata fields to update (merged with existing)\n\n        Raises:\n            KeyError: If extension is not installed\n        \"\"\"\n        extensions = self.data.get(\"extensions\")\n        if not isinstance(extensions, dict) or extension_id not in extensions:\n            raise KeyError(f\"Extension '{extension_id}' is not installed\")\n        # Merge new metadata with existing, preserving original installed_at\n        existing = extensions[extension_id]\n        # Handle corrupted registry entries (e.g., string/list instead of dict)\n        if not isinstance(existing, dict):\n            existing = {}\n        # Merge: existing fields preserved, new fields override (deep copy to prevent caller mutation)\n        merged = {**existing, **copy.deepcopy(metadata)}\n        # Always preserve original installed_at based on key existence, not truthiness,\n        # to handle cases where the field exists but may be falsy (legacy/corruption)\n        if \"installed_at\" in existing:\n            merged[\"installed_at\"] = existing[\"installed_at\"]\n        else:\n            # If not present in existing, explicitly remove from merged if caller provided it\n            merged.pop(\"installed_at\", None)\n        extensions[extension_id] = merged\n        self._save()\n\n    def restore(self, extension_id: str, metadata: dict):","sourceCodeStart":837,"sourceCodeEnd":873,"githubUrl":"https://github.com/github/spec-kit/blob/bf88c9f9a82fa370c7a7257aa2b3cf10b457b65c/src/specify_cli/extensions/__init__.py#L837-L873","documentation":"ExtensionRegistry.update_extension_metadata() raised KeyError because the given extension_id is not a key in the registry's 'extensions' dict (or the dict itself is missing/not a dict in a corrupted registry). Unlike the ValidationError family, this is a plain KeyError from the registry data layer, used by callers performing enable/disable or other metadata updates on already-installed extensions.","triggerScenarios":"Calling update_extension_metadata('myext', {...}) when 'myext' was never installed, was removed via remove(), or the registry file lost its entry (manual edit, partial uninstall, different .specify directory). The membership check on extensions dict fails and KeyError is raised.","commonSituations":"Running enable/disable on an extension ID with a typo; operating in the wrong project directory whose registry never had the extension; stale scripts referencing an extension removed by a teammate; registry JSON corrupted so 'extensions' is not a dict.","solutions":["Check the registry: inspect .specify/extensions/registry (or run the list-extensions CLI verb) to confirm the exact installed ID.","Install the extension first (specify extension install / add), then call the metadata update.","If the registry is corrupted, restore it from source control or reinstall the extension to rebuild the entry.","Guard the call with a membership check (see validationCode) to fail gracefully for absent IDs."],"exampleFix":"// before\nregistry.update_extension_metadata(\"my-ext\", {\"enabled\": True})  # KeyError if absent\n// after\nif \"my-ext\" in registry.data.get(\"extensions\", {}):\n    registry.update_extension_metadata(\"my-ext\", {\"enabled\": True})","handlingStrategy":"try-catch","validationCode":"exts = registry.data.get(\"extensions\")\nif not isinstance(exts, dict) or extension_id not in exts:\n    raise SystemExit(f\"extension '{extension_id}' is not installed; run install first\")\nregistry.update_extension_metadata(extension_id, {\"enabled\": True})","typeGuard":"def extension_is_installed(registry, extension_id: str) -> bool:\n    exts = registry.data.get(\"extensions\")\n    return isinstance(exts, dict) and extension_id in exts","tryCatchPattern":"try:\n    registry.update_extension_metadata(extension_id, metadata)\nexcept KeyError:\n    # extension absent: install it or surface an 'not installed' message to the user\n    ...","preventionTips":["Check the registry (or CLI list verb) for the exact ID before enable/disable.","Capture extension IDs from install output and reuse them verbatim.","Treat KeyError from registry updates as a state bug, not a crash."],"tags":["extensions","registry","key-error","state"],"backgroundTag":null,"analyzedSha":"bf88c9f9a82fa370c7a7257aa2b3cf10b457b65c","analyzedAt":"2026-08-14T19:43:37.150Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}