{"record":{"id":"8a63a1b269e72711","repo":"github/spec-kit","slug":"catalog-source-resolved-id-or-url-already-ex","errorCode":null,"errorMessage":"Catalog source '{resolved_id}' (or url) already exists in this project.","messagePattern":"Catalog source '(.+?)' \\(or url\\) already exists in this project\\.","errorType":"exception","errorClass":"BundlerError","httpStatus":null,"severity":"error","filePath":"src/specify_cli/bundler/commands_impl/catalog_config.py","lineNumber":191,"sourceCode":"        # or \"https://user@\". Validating here keeps junk out of\n        # bundle-catalogs.yml instead of failing later at fetch time.\n        is_localhost = hostname in (\"localhost\", \"127.0.0.1\", \"::1\")\n        if parsed.scheme.lower() != \"https\" and not is_localhost:\n            raise BundlerError(\n                f\"Catalog url must use HTTPS (got {parsed.scheme}://). \"\n                \"HTTP is only allowed for localhost.\"\n            )\n        if not hostname:\n            raise BundlerError(f\"Catalog url must be a valid URL with a host: {url}\")\n\n    url = _canonicalize_url(url)\n    install_policy = InstallPolicy.parse(policy)\n    resolved_id = (source_id or _derive_id(url)).strip()\n\n    catalogs = _read(project_root)\n    for existing in catalogs:\n        if existing.get(\"id\") == resolved_id or existing.get(\"url\") == url:\n            raise BundlerError(\n                f\"Catalog source '{resolved_id}' (or url) already exists in this project.\"\n            )\n\n    entry = {\n        \"id\": resolved_id,\n        \"url\": url,\n        \"priority\": int(priority),\n        \"install_policy\": install_policy.value,\n    }\n    catalogs.append(entry)\n    _write(project_root, catalogs)\n    return CatalogSource.from_dict(entry, Scope.PROJECT)\n\n\ndef remove_source(project_root: Path, id_or_url: str) -> str:\n    target = id_or_url.strip()\n    if target in _BUILTIN_IDS:\n        raise BundlerError(","sourceCodeStart":173,"sourceCodeEnd":209,"githubUrl":"https://github.com/github/spec-kit/blob/bf88c9f9a82fa370c7a7257aa2b3cf10b457b65c/src/specify_cli/bundler/commands_impl/catalog_config.py#L173-L209","documentation":"Raised by add_source() when registering a catalog source whose resolved id or canonical url collides with an entry already present in the project's catalog config. The function canonicalizes the url (e.g. expands relative local paths to absolute file paths) and derives an id from it when none is supplied, so the duplicate check covers both explicit and derived collisions. This protects the project catalog list from ambiguous, shadowing entries.","triggerScenarios":"Calling add_source(project_root, url=...) twice with the same url; calling add_source with a source_id equal to an existing entry's id; adding a local path that _canonicalize_url() expands to the same absolute url as a previously added source; adding a built-in id that already appears in project scope.","commonSituations":"Re-running a setup script or 'specify bundler catalog add' command twice; adding a source that was already added in an earlier session; copy-pasting onboarding docs that add the same default catalog; CI provisioning steps that run unconditionally instead of checking current state.","solutions":["Remove the existing source first: remove_source(project_root, '<existing-id-or-url>'), then re-add it with the new settings.","Check the current project catalog (via the list/read API or the catalog config file) before calling add_source, and skip when the id or canonical url is already present.","If the collision is only on url but you want a distinct id, pass an explicit source_id and a different url (or vice versa) only if a genuinely different source is intended.","Make idempotent provisioning scripts tolerant: catch this BundlerError and treat 'already exists' as success."],"exampleFix":"# before\nadd_source(root, \"community\", \"https://example.com/catalog.json\")  # second run raises\n\n# after\nexisting = {c.id: c for c in read_sources(root)}\nif \"community\" not in existing:\n    add_source(root, \"community\", \"https://example.com/catalog.json\")","handlingStrategy":"validation","validationCode":"from specify_cli.bundler.commands_impl import catalog_config\n\ndef add_source_safe(root, source_id, url, priority, policy):\n    existing = catalog_config._read(root)\n    for c in existing:\n        if c.get(\"id\") == source_id or c.get(\"url\") == url:\n            return None  # already present\n    return catalog_config.add_source(root, source_id, url, priority, policy)","typeGuard":null,"tryCatchPattern":"try:\n    add_source(root, sid, url, priority=10, policy=\"install-allowed\")\nexcept BundlerError as e:\n    if \"already exists\" in str(e):\n        pass  # idempotent provisioning: treat as success\n    else:\n        raise","preventionTips":["Make provisioning scripts idempotent: check the catalog list before adding.","Use stable, explicit source_ids so url changes do not create accidental duplicates.","Wrap add/remove flows in a single helper that catches 'already exists'/'not found' as no-ops."],"tags":["bundler","catalog","duplicate","configuration"],"backgroundTag":null,"analyzedSha":"bf88c9f9a82fa370c7a7257aa2b3cf10b457b65c","analyzedAt":"2026-08-14T19:43:37.150Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}