moeru-ai/airi · error · Error

Spine ZIP must contain a .skel or .json skeleton file paired

Error message

Spine ZIP must contain a .skel or .json skeleton file paired with a .atlas

What it means

`detectSpineLayout(entries, atlasText)` returns the first matched skeleton+atlas pair by calling `detectAllSpineLayouts`. If no variant is found (`variants.length === 0`) it throws `Spine ZIP must contain a .skel or .json skeleton file paired with a .atlas`. It requires a same-basename skeleton (`.skel` or `.json`) alongside an `.atlas`/`.atlas.txt`.

Source

Thrown at packages/stage-ui-spine/src/utils/spine-zip-loader.ts:106

function dirname(path: string) {
  const slash = path.lastIndexOf('/')
  return slash === -1 ? '' : path.slice(0, slash + 1)
}

/**
 * Inspect a Spine ZIP and resolve the skeleton, atlas, and texture page
 * paths.
 *
 * Heuristics:
 * 1. Find all `.atlas`/`.atlas.txt` files paired with same-basename skeletons.
 * 2. For each pair, walk the atlas to extract texture page filenames.
 * 3. Return the first matched pair as the primary layout.
 */
export function detectSpineLayout(entries: Record<string, string>, atlasText: Record<string, string>): SpineModelLayout {
  const variants = detectAllSpineLayouts(entries, atlasText)
  if (variants.length === 0)
    throw new Error('Spine ZIP must contain a .skel or .json skeleton file paired with a .atlas')
  return variants[0].layout
}

/**
 * Detects all skeleton+atlas pairs in a ZIP, returning them as named
 * variants. Useful for ZIPs containing multiple outfits/characters in
 * separate folders.
 */
export function detectAllSpineLayouts(entries: Record<string, string>, atlasText: Record<string, string>): SpineModelVariant[] {
  const allFiles = Object.keys(entries)
  const atlasCandidates = allFiles.filter(isAtlasPath)
  if (atlasCandidates.length === 0)
    throw new Error('Spine ZIP must contain a .atlas (or .atlas.txt) file')

  const variants: SpineModelVariant[] = []
  const usedAtlases = new Set<string>()

  // First pass: match each atlas with a same-basename skeleton.

View on GitHub (pinned to 27111382b4)

Solutions

  1. Verify the archive has a `<name>.atlas` (or `.atlas.txt`) paired with a `<name>.skel` or `<name>.json` of the same basename in the same folder.
  2. Re-export from Spine with 'Texture packer' producing the `.atlas` alongside the skeleton.
  3. Rename files so skeleton and atlas basenames match exactly.
  4. If multiple characters exist, use `detectAllSpineLayouts` to inspect available pairs.

Example fix

// before
const layout = detectSpineLayout(entries, atlasText)  // throws

// after
const variants = detectAllSpineLayouts(entries, atlasText)
if (!variants.length) throw new Error('No skeleton+atlas pair with matching basename found')
const layout = variants[0].layout
Defensive patterns

Strategy: validation

Validate before calling

const variants = detectAllSpineLayouts(entries, atlasText)
if (!variants.length)
  throw new Error('No skeleton+atlas pair with matching basename')
const layout = variants[0].layout

Type guard

function hasSpinePair(entries: Record<string, string>, atlasText: Record<string, string>): boolean {
  return detectAllSpineLayouts(entries, atlasText).length > 0
}

Try / catch

try {
  detectSpineLayout(entries, atlasText)
} catch (e) {
  if (e instanceof Error && e.message.includes('paired with a .atlas')) {
    // inspect available files and re-prompt
  } else throw e
}

Prevention

When it happens

Trigger: Calling `detectSpineLayout` with an `entries` map that has an atlas but no matching skeleton, a skeleton but no atlas, or skeleton/atlas with mismatched basenames (`hero.skel` + `character.atlas`). Also when `entries` is empty or only contains textures.

Common situations: Spine archive missing the `.atlas` or the `.skel`/`.json`; renamed files breaking the basename pairing; multiple characters in one archive with only one fully paired; a Spine Lite vs Spine Pro export where the skeleton extension differs.

Related errors


AI-assisted analysis of moeru-ai/airi@27111382b4 (2026-08-12). Data as JSON: /api/errors/259a51fa57c5a949. Report an issue: GitHub.