{"record":{"id":"28f7b429823a5f6a","repo":"moeru-ai/airi","slug":"extension-source-must-be-a-regular-directory-sourcepath","errorCode":null,"errorMessage":"Extension source must be a regular directory: ${sourcePath}","messagePattern":"Extension source must be a regular directory: (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"apps/stage-tamagotchi/src/main/services/airi/plugins/host/directory-import.ts","lineNumber":155,"sourceCode":"      throw new Error('Extension manifest exceeds the 1 MiB size limit.')\n    }\n\n    const finalStats = await handle.stat()\n    if (!finalStats.isFile() || finalStats.size !== expectedSize || bytesRead !== expectedSize) {\n      throw new Error('Extension package changed during inspection. Select the folder again.')\n    }\n\n    return buffer.subarray(0, bytesRead)\n  }\n  finally {\n    await handle.close()\n  }\n}\n\nasync function inspectExtensionDirectory(sourcePath: string): Promise<InspectedExtensionDirectory> {\n  const sourceStats = await lstat(sourcePath)\n  if (sourceStats.isSymbolicLink() || !sourceStats.isDirectory()) {\n    throw new Error(`Extension source must be a regular directory: ${sourcePath}`)\n  }\n\n  const sourceRealPath = await realpath(sourcePath)\n  const manifestPath = join(sourceRealPath, extensionManifestFileName)\n  await assertRegularFile(manifestPath, 'Extension manifest')\n\n  const files: Array<{ path: string, relativePath: string, size: number }> = []\n  const directories = ['.']\n  const pendingDirectories = [sourceRealPath]\n  let entryCount = 0\n  let totalBytes = 0\n  while (pendingDirectories.length > 0) {\n    const directory = pendingDirectories.pop()\n    if (!directory) {\n      continue\n    }\n    const entries = await opendir(directory)\n    for await (const entry of entries) {","sourceCodeStart":137,"sourceCodeEnd":173,"githubUrl":"https://github.com/moeru-ai/airi/blob/438a067dde47aa0bdb46c2323d1fe293dc805218/apps/stage-tamagotchi/src/main/services/airi/plugins/host/directory-import.ts#L137-L173","documentation":"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.","triggerScenarios":"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).","commonSituations":"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.","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`."],"exampleFix":"// before\nawait inspected('/opt/extensions/latest') // 'latest' is a symlink -> throws\n// after\nawait inspected(await realpath('/opt/extensions/latest'))","handlingStrategy":"validation","validationCode":"import { lstat, realpath } from 'node:fs/promises'\nasync function assertImportableDirectory(path: string) {\n  const stats = await lstat(path)\n  if (stats.isSymbolicLink() || !stats.isDirectory())\n    throw new Error(`${path} must be a real directory, not a symlink or file.`)\n  return realpath(path)\n}","typeGuard":null,"tryCatchPattern":"try {\n  await inspected(path)\n} catch (error) {\n  if (error instanceof Error && error.message.startsWith('Extension source must be a regular directory')) {\n    showUserError('Select the extracted extension folder itself, not a link or file.')\n    return\n  }\n  throw error\n}","preventionTips":["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."],"tags":["filesystem","validation","symlink","extensions"],"backgroundTag":"path-is-not-a-directory","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"}