moeru-ai/airi · error · Error
Extension source must be a regular directory
Error message
Extension source must be a regular directory: ${sourcePath} What it means
inspectExtensionDirectory rejects any source path that is a symbolic link or not a directory, checked via lstat before any traversal. Extension packages must be plain, self-contained folders so that the entry walk, size accounting, and symlink prohibition cannot be bypassed or confused by links.
Solutions
- Select the real, physical directory, not a symlink to it.
- Extract the archive first and import the extracted folder, not the .zip file.
- Resolve or replace symlinks: copy -rL the source to a real directory and import that.
- Confirm the path is a directory: `ls -ld <path>` should show `d...` with no leading `l`.
Example fix
// before
await inspected('/opt/extensions/latest') // 'latest' is a symlink -> throws
// after
await inspected(await realpath('/opt/extensions/latest')) Defensive patterns
Strategy: validation
Validate before calling
import { lstat, realpath } from 'node:fs/promises'
async function assertImportableDirectory(path: string) {
const stats = await lstat(path)
if (stats.isSymbolicLink() || !stats.isDirectory())
throw new Error(`${path} must be a real directory, not a symlink or file.`)
return realpath(path)
} Try / catch
try {
await inspected(path)
} catch (error) {
if (error instanceof Error && error.message.startsWith('Extension source must be a regular directory')) {
showUserError('Select the extracted extension folder itself, not a link or file.')
return
}
throw error
} Prevention
- Always extract archives before importing; never point the picker at the .zip.
- Resolve symlinks with realpath before passing a path to the import API.
- Check `ls -ld <path>` for a leading `l` or a non-`d` type before selecting.
When it happens
Trigger: Calling inspectExtensionDirectory (via `inspected` or `staged`) with a path that resolves to a symlink to a directory, a regular file, a FIFO/socket/device, or a missing path (lstat ENOENT fails earlier, but dangling symlinks land here).
Common situations: User drags a symlink like `my-extension-latest -> my-extension-1.2.0` into the import dialog; user selects a zip file instead of an extracted folder; a path inside a symlinked project directory is picked; selecting the manifest file itself instead of its folder.
Related errors
- Extension package exceeds the
- Extension packages can contain only files and directories
- Extension packages cannot contain symbolic links
- Extension entrypoint resolves outside the package folder
- Extension manifest does not exist
AI-assisted analysis of moeru-ai/airi@438a067dde (2026-09-17).
Data as JSON: /api/errors/28f7b429823a5f6a.
Report an issue: GitHub.
Appendix: source
Thrown at apps/stage-tamagotchi/src/main/services/airi/plugins/host/directory-import.ts:155
throw new Error('Extension manifest exceeds the 1 MiB size limit.')
}
const finalStats = await handle.stat()
if (!finalStats.isFile() || finalStats.size !== expectedSize || bytesRead !== expectedSize) {
throw new Error('Extension package changed during inspection. Select the folder again.')
}
return buffer.subarray(0, bytesRead)
}
finally {
await handle.close()
}
}
async function inspectExtensionDirectory(sourcePath: string): Promise<InspectedExtensionDirectory> {
const sourceStats = await lstat(sourcePath)
if (sourceStats.isSymbolicLink() || !sourceStats.isDirectory()) {
throw new Error(`Extension source must be a regular directory: ${sourcePath}`)
}
const sourceRealPath = await realpath(sourcePath)
const manifestPath = join(sourceRealPath, extensionManifestFileName)
await assertRegularFile(manifestPath, 'Extension manifest')
const files: Array<{ path: string, relativePath: string, size: number }> = []
const directories = ['.']
const pendingDirectories = [sourceRealPath]
let entryCount = 0
let totalBytes = 0
while (pendingDirectories.length > 0) {
const directory = pendingDirectories.pop()
if (!directory) {
continue
}
const entries = await opendir(directory)
for await (const entry of entries) {View on GitHub (pinned to 438a067dde)