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
- 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.
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
- 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.
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
- Extension copy does not match the reviewed package.
- Extension package changed during copy. Select the folder…
- does not exist
- Extension directory importer is disposed.
- Extension import plan is missing or was already used.
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)