ituoke/scripts/stage-filesystem-runtime.mjs

53 lines
4.2 KiB
JavaScript

// Build-machine only. Clients run a private Node and locked production packages.
import { cp, mkdir, readFile, writeFile, copyFile, stat, rm } from 'node:fs/promises'
import { fileURLToPath } from 'node:url'
import path from 'node:path'
import { spawnSync } from 'node:child_process'
const root = fileURLToPath(new URL('../', import.meta.url))
const source = path.join(root, 'runtime/filesystem')
const dest = path.join(root, 'third_party/mcp-filesystem')
const check = process.argv.includes('--check')
const nodeName = process.platform === 'win32' ? 'node.exe' : 'node'
function run(command, args, options = {}) {
const result = spawnSync(command, args, { encoding: 'utf8', ...options })
if (result.error || result.status !== 0) throw result.error || Error(result.stderr || `${command} failed`)
return result.stdout.trim()
}
if (!['win32', 'darwin'].includes(process.platform)) throw Error('Stage Filesystem MCP on the target Windows or macOS build machine')
if (Number(process.versions.node.split('.')[0]) < 24) throw Error('Build machine needs Node >= 24; customers do not need Node')
if (!check) {
// npm ci uses the committed integrity-checked lockfile, never @latest on clients.
if (!process.env.npm_execpath) throw Error('Run npm --prefix runtime/filesystem run stage')
run(process.execPath, [process.env.npm_execpath, 'ci', '--omit=dev', '--ignore-scripts', '--no-audit', '--no-fund'], { cwd: source })
await mkdir(dest, { recursive: true })
const modules = path.resolve(dest, 'node_modules')
if (path.dirname(modules) !== path.resolve(root, 'third_party/mcp-filesystem')) throw Error('Invalid staging directory')
await rm(modules, { recursive: true, force: true })
await cp(path.join(source, 'node_modules'), modules, { recursive: true, dereference: true })
const nodeSource = process.platform === 'darwin' && process.env.ITUOKE_NODE_UNIVERSAL_SOURCE || process.execPath
await copyFile(nodeSource, path.join(dest, nodeName))
await copyFile(path.join(source, 'package-lock.json'), path.join(dest, 'package-lock.json'))
await copyFile(path.join(source, 'FILESYSTEM-LICENSE.txt'), path.join(dest, 'FILESYSTEM-LICENSE.txt'))
await copyFile(path.join(source, 'FILESYSTEM-LICENSE.txt'), path.join(dest, 'FILESYSTEM-LICENSE.txt'))
// Node distributions include their license adjacent to the executable.
const license = process.env.ITUOKE_NODE_LICENSE || path.join(path.dirname(process.execPath), 'LICENSE')
await copyFile(license, path.join(dest, 'NODE-LICENSE.txt'))
await writeFile(path.join(dest, 'runtime.json'), JSON.stringify({ platform: process.platform, arch: nodeSource === process.execPath ? process.arch : 'universal', node: process.versions.node, filesystem: '2.1.0' }, null, 2))
}
const manifest = JSON.parse(await readFile(path.join(dest, 'runtime.json'), 'utf8'))
if (manifest.platform !== process.platform || (manifest.arch !== process.arch && manifest.arch !== 'universal') || manifest.filesystem !== '2.1.0') throw Error('Filesystem runtime platform/version mismatch; restage on this build machine')
const actual = run(path.join(dest, nodeName), ['-p', 'JSON.stringify({platform:process.platform,arch:process.arch,major:Number(process.versions.node.split(".")[0])})'])
const identity = JSON.parse(actual)
if (identity.major < 24 || identity.platform !== manifest.platform || (manifest.arch !== 'universal' && identity.arch !== manifest.arch)) throw Error('Wrong bundled Node runtime')
if (process.platform === 'darwin') {
run('lipo', ['-verify_arch', ...(manifest.arch === 'universal' ? ['arm64', 'x86_64'] : [manifest.arch === 'x64' ? 'x86_64' : manifest.arch]), path.join(dest, nodeName)])
const libraries = run('otool', ['-L', path.join(dest, nodeName)]).split('\n').slice(1)
if (libraries.some(line => !/^\s*\/(usr\/lib|System\/Library)\//.test(line))) throw Error('Node links to non-system dylibs; use an official self-contained Node distribution')
}
await stat(path.join(dest, 'node_modules/@modelcontextprotocol/server-filesystem/dist/index.js'))
await stat(path.join(dest, 'NODE-LICENSE.txt'))
await stat(path.join(dest, 'FILESYSTEM-LICENSE.txt'))
await stat(path.join(dest, 'FILESYSTEM-LICENSE.txt'))
console.log(`Filesystem MCP 2.1.0 staged: ${manifest.platform}/${manifest.arch}, Node ${manifest.node}`)