{"record":{"id":"849f5fdfd17f099f","repo":"moeru-ai/airi","slug":"extension-is-already-installed-extensionid","errorCode":null,"errorMessage":"Extension is already installed: ${extensionId}","messagePattern":"Extension is already installed: (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"apps/stage-tamagotchi/src/main/services/airi/plugins/host/directory-import.ts","lineNumber":473,"sourceCode":"      return await operation()\n    }\n    if (!this.startAccessingSecurityScopedResource) {\n      throw new Error('Security-scoped Extension import access is not configured.')\n    }\n\n    const stopAccessing = this.startAccessingSecurityScopedResource(bookmark)\n    try {\n      return await operation()\n    }\n    finally {\n      stopAccessing()\n    }\n  }\n\n  private async assertDestinationAvailable(extensionId: string): Promise<void> {\n    const destination = join(this.extensionsRoot, extensionId)\n    if (await this.isExtensionInstalled(extensionId) || await pathExists(destination)) {\n      throw new Error(`Extension is already installed: ${extensionId}`)\n    }\n  }\n\n  private async commitPreparedPlan(planId: string): Promise<ManifestEntry> {\n    const storedPlan = this.plans.get(planId)\n    if (!storedPlan) {\n      throw new Error('Extension import plan is missing or was already used.')\n    }\n    return await this.withSecurityScopedAccess(storedPlan.securityScopedBookmark, async () => {\n      const inspected = await inspectExtensionDirectory(storedPlan.sourcePath)\n      if (inspected.manifest.id !== storedPlan.preview.extensionId || inspected.fingerprint !== storedPlan.fingerprint) {\n        throw new Error('Extension source changed after review. Select the folder again.')\n      }\n\n      await this.assertDestinationAvailable(inspected.manifest.id)\n      await mkdir(this.extensionsRoot, { recursive: true })\n      const stagingRoot = join(this.extensionsRoot, '.imports')\n      await mkdir(stagingRoot, { recursive: true })","sourceCodeStart":455,"sourceCodeEnd":491,"githubUrl":"https://github.com/moeru-ai/airi/blob/438a067dde47aa0bdb46c2323d1fe293dc805218/apps/stage-tamagotchi/src/main/services/airi/plugins/host/directory-import.ts#L455-L491","documentation":"assertDestinationAvailable guards the extensions root against overwriting an existing extension. It throws when either the service reports the extensionId as installed or a directory already exists at <extensionsRoot>/<extensionId>. This is a deliberate idempotency/collision check before any files are copied or renamed into place.","triggerScenarios":"prepare() or commitPreparedPlan() called for an extensionId whose directory already exists under extensionsRoot, or whose isExtensionInstalled() check returns true (e.g. re-importing the same extension folder twice, importing a copy of an already-installed extension with the same manifest id).","commonSituations":"User drags the same extension folder a second time; two different folders share the same manifest id; a previous import left a partial/abandoned directory at the destination; stale extension store state after the folder was manually deleted but the store still says installed (or vice versa).","solutions":["Uninstall or remove the existing extension with that id first, then retry the import.","If the destination is a leftover from a failed import, delete <extensionsRoot>/<extensionId> and any <extensionsRoot>/.imports staging directory, then retry.","Pick the extension folder whose manifest.id is unique (edit the manifest id if you are importing a renamed copy).","If the store's installed-state is stale, resync isExtensionInstalled/manifest state with the filesystem and retry."],"exampleFix":"null","handlingStrategy":"validation","validationCode":"const destination = join(extensionsRoot, manifest.id)\nif (await pathExists(destination)) {\n  throw new Error(`Uninstall or remove the existing extension at ${destination} before importing.`)\n}","typeGuard":null,"tryCatchPattern":"try {\n  await importService.commit(planId)\n} catch (error) {\n  if (error.message.startsWith('Extension is already installed')) {\n    const id = error.message.split(': ')[1]\n    await uninstallExtension(id) // then retry the import\n  }\n}","preventionTips":["Check for an installed extension with the same manifest id before starting an import.","Clean up abandoned directories under extensionsRoot and extensionsRoot/.imports after failed imports.","Give distinct manifest ids to locally modified copies of extensions."],"tags":["filesystem","collision","extension-import"],"backgroundTag":"file-already-exists","analyzedSha":"438a067dde47aa0bdb46c2323d1fe293dc805218","analyzedAt":"2026-09-17T01:14:42.644Z","contentChangedAt":"2026-09-17T01:14:42.644Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}