moeru-ai/airi · error

Extension is already installed

Error message

Extension is already installed: ${extensionId}

What it means

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.

Solutions

  1. Uninstall or remove the existing extension with that id first, then retry the import.
  2. If the destination is a leftover from a failed import, delete <extensionsRoot>/<extensionId> and any <extensionsRoot>/.imports staging directory, then retry.
  3. Pick the extension folder whose manifest.id is unique (edit the manifest id if you are importing a renamed copy).
  4. If the store's installed-state is stale, resync isExtensionInstalled/manifest state with the filesystem and retry.

Example fix

null
Defensive patterns

Strategy: validation

Validate before calling

const destination = join(extensionsRoot, manifest.id)
if (await pathExists(destination)) {
  throw new Error(`Uninstall or remove the existing extension at ${destination} before importing.`)
}

Try / catch

try {
  await importService.commit(planId)
} catch (error) {
  if (error.message.startsWith('Extension is already installed')) {
    const id = error.message.split(': ')[1]
    await uninstallExtension(id) // then retry the import
  }
}

Prevention

When it happens

Trigger: 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).

Common situations: 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).

Understand the failure class

Background: "already exists" / EEXIST / FileAlreadyExistsException: what the 'file already exists' error means and how to fix it — this error's family across 37 libraries.

Related errors


AI-assisted analysis of moeru-ai/airi@438a067dde (2026-09-17). Data as JSON: /api/errors/849f5fdfd17f099f. Report an issue: GitHub.

Appendix: source

Thrown at apps/stage-tamagotchi/src/main/services/airi/plugins/host/directory-import.ts:473

      return await operation()
    }
    if (!this.startAccessingSecurityScopedResource) {
      throw new Error('Security-scoped Extension import access is not configured.')
    }

    const stopAccessing = this.startAccessingSecurityScopedResource(bookmark)
    try {
      return await operation()
    }
    finally {
      stopAccessing()
    }
  }

  private async assertDestinationAvailable(extensionId: string): Promise<void> {
    const destination = join(this.extensionsRoot, extensionId)
    if (await this.isExtensionInstalled(extensionId) || await pathExists(destination)) {
      throw new Error(`Extension is already installed: ${extensionId}`)
    }
  }

  private async commitPreparedPlan(planId: string): Promise<ManifestEntry> {
    const storedPlan = this.plans.get(planId)
    if (!storedPlan) {
      throw new Error('Extension import plan is missing or was already used.')
    }
    return await this.withSecurityScopedAccess(storedPlan.securityScopedBookmark, async () => {
      const inspected = await inspectExtensionDirectory(storedPlan.sourcePath)
      if (inspected.manifest.id !== storedPlan.preview.extensionId || inspected.fingerprint !== storedPlan.fingerprint) {
        throw new Error('Extension source changed after review. Select the folder again.')
      }

      await this.assertDestinationAvailable(inspected.manifest.id)
      await mkdir(this.extensionsRoot, { recursive: true })
      const stagingRoot = join(this.extensionsRoot, '.imports')
      await mkdir(stagingRoot, { recursive: true })

View on GitHub (pinned to 438a067dde)