moeru-ai/airi · error · Error

must be a regular file

Error message

${label} must be a regular file: ${path}

What it means

assertRegularFile rejects paths that are symlinks or not regular files, because manifests and package contents must be real files inside the imported directory. This hardens the extension importer against symlink-based escapes and special files (FIFOs, devices, directories).

Solutions

  1. Replace the symlink with a real copy of the file in the extension folder
  2. Ensure the labeled path is a regular file, not a directory or special file
  3. Avoid packaging extensions with symlinks; zip real files

Example fix

// before
ln -s ~/shared/package.json extensions/myext/package.json
// after
cp ~/shared/package.json extensions/myext/package.json
Defensive patterns

Strategy: validation

Validate before calling

import { lstat } from 'node:fs/promises'
async function isRegularFile(p: string): Promise<boolean> {
  try {
    const s = await lstat(p)
    return s.isFile() && !s.isSymbolicLink()
  } catch { return false }
}

Type guard

function isRegularStats(s: { isFile(): boolean; isSymbolicLink(): boolean }): boolean {
  return s.isFile() && !s.isSymbolicLink()
}

Try / catch

try {
  await importFromDirectory(dir)
} catch (e) {
  if (e instanceof Error && /must be a regular file:/.test(e.message)) {
    console.warn(`${e.message} — replace symlinks with real copies before importing`)
    return null
  }
  throw e
}

Prevention

When it happens

Trigger: The manifest (or other labeled file) is a symlink, a directory, or a device/fifo at the inspected path.

Common situations: Repository checkouts where package.json is a symlink to a shared config; macOS alias-like links in downloaded archives; pointing at a directory instead of the manifest file.

Understand the failure class

Background: "is not a compatible type" / "cannot merge" errors: when a value's type doesn't match what the library requires — this error's family across 65 libraries.

Related errors


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

Appendix: source

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

      optional: kit.optional ?? false,
    })),
  ]
}

async function assertRegularFile(path: string, label: string): Promise<Stats> {
  let stats: Stats
  try {
    stats = await lstat(path)
  }
  catch (error) {
    if (isMissingPathError(error)) {
      throw new Error(`${label} does not exist: ${path}`)
    }
    throw error
  }

  if (stats.isSymbolicLink() || !stats.isFile()) {
    throw new Error(`${label} must be a regular file: ${path}`)
  }
  return stats
}

async function readManifestSnapshot(path: string, expectedSize: number): Promise<Buffer> {
  const handle = await open(path, 'r')
  try {
    // One extra byte distinguishes the maximum valid manifest from a file
    // that grew past the limit after the directory walk.
    const buffer = Buffer.allocUnsafe(extensionPackageLimits.manifestBytes + 1)
    let bytesRead = 0
    while (bytesRead < buffer.byteLength) {
      const result = await handle.read(buffer, bytesRead, buffer.byteLength - bytesRead, bytesRead)
      if (result.bytesRead === 0) {
        break
      }
      bytesRead += result.bytesRead
    }

View on GitHub (pinned to 438a067dde)