Files
SpineParticlesWeb/connector/spine-cli.mjs
T
2026-09-05 16:20:23 +08:00

76 lines
2.9 KiB
JavaScript

import { access, stat } from 'node:fs/promises'
import { constants } from 'node:fs'
import { homedir, platform } from 'node:os'
import { join } from 'node:path'
import { execFile } from 'node:child_process'
const SUPPORTED_VERSIONS = new Set(['3.8', '4.0', '4.1', '4.2', '4.3'])
async function executable(path) {
if (!path) return false
try {
await access(path, platform() === 'win32' ? constants.F_OK : constants.X_OK)
return true
} catch {
return false
}
}
export async function findSpineExecutable() {
const candidates = []
if (process.env.SPINE_EXECUTABLE) candidates.push(process.env.SPINE_EXECUTABLE)
if (platform() === 'darwin') {
candidates.push(
'/Applications/Spine.app/Contents/MacOS/Spine',
join(homedir(), 'Applications/Spine.app/Contents/MacOS/Spine'),
)
} else if (platform() === 'win32') {
const programFiles = [process.env.ProgramFiles, process.env['ProgramFiles(x86)'], process.env.LOCALAPPDATA].filter(Boolean)
for (const root of programFiles) {
candidates.push(join(root, 'Spine', 'Spine.com'), join(root, 'Esoteric Software', 'Spine', 'Spine.com'))
}
} else {
candidates.push('/opt/Spine/Spine.sh', join(homedir(), 'Spine/Spine.sh'), '/usr/local/bin/Spine', '/usr/bin/Spine')
}
for (const candidate of candidates) if (await executable(candidate)) return candidate
return null
}
export function normalizeSpineVersion(value) {
const match = /^(3\.8|4\.[0-3])(?:\.\d+)?$/.exec(String(value || '').trim())
if (!match || !SUPPORTED_VERSIONS.has(match[1])) throw new Error(`不支持的 Spine 版本:${value}`)
return match[1]
}
function run(executablePath, args, timeoutMs = 180000) {
return new Promise((resolve, reject) => {
execFile(executablePath, args, { timeout: timeoutMs, windowsHide: true, maxBuffer: 4 * 1024 * 1024 }, (error, stdout, stderr) => {
if (error) {
const details = [stdout, stderr, error.message]
.filter(Boolean)
.join('\n')
.replace(/^Licensed to:.*$/gim, 'Spine 授权信息已隐藏')
.trim()
reject(new Error(details || 'Spine 命令执行失败'))
return
}
resolve({ stdout, stderr })
})
})
}
export async function importSpineProject({ executablePath, inputPath, outputPath, projectName, spineVersion }) {
const version = normalizeSpineVersion(spineVersion)
const editorVersion = `${version}.xx`
const result = await run(executablePath, [
'--update', editorVersion,
'--input', inputPath,
'--output', outputPath,
'--import', projectName,
])
const output = await stat(outputPath).catch(() => null)
if (!output?.isFile() || output.size === 0) throw new Error('Spine 未生成有效的工程文件')
const detected = /Starting:\s+Spine\s+([0-9.]+)/i.exec(`${result.stdout}\n${result.stderr}`)?.[1] || editorVersion
return { ...result, editorVersion: detected, size: output.size }
}