mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-08-26 00:53:48 +00:00
220 lines
6 KiB
JavaScript
220 lines
6 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* @license
|
|
* Copyright 2026 Qwen Team
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
import { spawnSync } from 'node:child_process';
|
|
import {
|
|
existsSync,
|
|
mkdirSync,
|
|
mkdtempSync,
|
|
readFileSync,
|
|
readdirSync,
|
|
rmSync,
|
|
writeFileSync,
|
|
} from 'node:fs';
|
|
import { tmpdir } from 'node:os';
|
|
import path from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
|
|
|
const scriptDirectory = path.dirname(fileURLToPath(import.meta.url));
|
|
const packageRoot = path.resolve(scriptDirectory, '..');
|
|
const npmRegistry = 'https://registry.npmjs.org';
|
|
|
|
function valueAfter(flag) {
|
|
const index = process.argv.indexOf(flag);
|
|
if (index < 0 || !process.argv[index + 1]) {
|
|
throw new Error(`missing ${flag}`);
|
|
}
|
|
return process.argv[index + 1];
|
|
}
|
|
|
|
function run(command, args, options = {}) {
|
|
const result = spawnSync(command, args, {
|
|
cwd: options.cwd ?? packageRoot,
|
|
encoding: options.capture ? 'utf8' : undefined,
|
|
stdio: options.capture ? 'pipe' : 'inherit',
|
|
});
|
|
if (result.error) throw result.error;
|
|
if (result.status !== 0) {
|
|
const detail = options.capture ? `\n${result.stderr || result.stdout}` : '';
|
|
throw new Error(`${command} exited with status ${result.status}${detail}`);
|
|
}
|
|
return result.stdout;
|
|
}
|
|
|
|
const outputDirectory = path.resolve(valueAfter('--output-dir'));
|
|
const manifest = JSON.parse(
|
|
readFileSync(path.join(packageRoot, 'package.json'), 'utf8'),
|
|
);
|
|
if (manifest.name !== '@qwen-code/node-repl-mcp') {
|
|
throw new Error(`unexpected package name ${manifest.name}`);
|
|
}
|
|
|
|
mkdirSync(outputDirectory, { recursive: true });
|
|
run('npm', ['run', 'build']);
|
|
const packOutput = JSON.parse(
|
|
run('npm', ['pack', '.', '--pack-destination', outputDirectory, '--json'], {
|
|
capture: true,
|
|
}),
|
|
);
|
|
if (!Array.isArray(packOutput) || packOutput.length !== 1) {
|
|
throw new Error('npm pack did not produce exactly one package');
|
|
}
|
|
|
|
const packed = packOutput[0];
|
|
if (packed.name !== manifest.name || packed.version !== manifest.version) {
|
|
throw new Error(
|
|
`packed identity ${packed.name}@${packed.version} does not match source`,
|
|
);
|
|
}
|
|
const packedPaths = packed.files.map((entry) => entry.path);
|
|
for (const required of [
|
|
'LICENSE',
|
|
'README.md',
|
|
'dist/index.js',
|
|
'dist/index.d.ts',
|
|
'dist/kernel-manager.js',
|
|
'dist/mcp-server.js',
|
|
'dist/runtime/kernel.mjs',
|
|
'dist/runtime/module-loader.mjs',
|
|
'dist/runtime/tree-sitter-javascript.wasm',
|
|
]) {
|
|
if (!packedPaths.includes(required)) {
|
|
throw new Error(`packed Node REPL is missing ${required}`);
|
|
}
|
|
}
|
|
const forbidden = packedPaths.find(
|
|
(entry) => entry.startsWith('src/') || entry.includes('.test.'),
|
|
);
|
|
if (forbidden) {
|
|
throw new Error(`packed Node REPL contains development source: ${forbidden}`);
|
|
}
|
|
|
|
const tarball = path.join(outputDirectory, packed.filename);
|
|
const tarballs = readdirSync(outputDirectory).filter((entry) =>
|
|
entry.endsWith('.tgz'),
|
|
);
|
|
if (tarballs.length !== 1 || tarballs[0] !== packed.filename) {
|
|
throw new Error(
|
|
`expected exactly one Node REPL tarball, found ${tarballs.join(', ')}`,
|
|
);
|
|
}
|
|
run('npm', [
|
|
'publish',
|
|
tarball,
|
|
'--dry-run',
|
|
'--access',
|
|
'public',
|
|
'--json',
|
|
`--registry=${npmRegistry}`,
|
|
]);
|
|
|
|
const consumer = mkdtempSync(path.join(tmpdir(), 'qwen-node-repl-consumer-'));
|
|
const textOf = (result) =>
|
|
(result.content ?? [])
|
|
.filter((block) => block.type === 'text')
|
|
.map((block) => block.text)
|
|
.join('');
|
|
|
|
try {
|
|
writeFileSync(
|
|
path.join(consumer, 'package.json'),
|
|
`${JSON.stringify(
|
|
{ name: 'node-repl-release-smoke', private: true, type: 'module' },
|
|
null,
|
|
2,
|
|
)}\n`,
|
|
);
|
|
run(
|
|
'npm',
|
|
[
|
|
'install',
|
|
tarball,
|
|
'--no-audit',
|
|
'--no-fund',
|
|
`--registry=${npmRegistry}`,
|
|
],
|
|
{ cwd: consumer },
|
|
);
|
|
|
|
const installedRoot = path.join(
|
|
consumer,
|
|
'node_modules',
|
|
'@qwen-code',
|
|
'node-repl-mcp',
|
|
);
|
|
const installedManifest = JSON.parse(
|
|
readFileSync(path.join(installedRoot, 'package.json'), 'utf8'),
|
|
);
|
|
if (
|
|
installedManifest.name !== manifest.name ||
|
|
installedManifest.version !== manifest.version
|
|
) {
|
|
throw new Error('clean install resolved the wrong Node REPL package');
|
|
}
|
|
const serverEntry = path.join(installedRoot, 'dist', 'index.js');
|
|
if (!existsSync(serverEntry)) {
|
|
throw new Error('clean install is missing the Node REPL entry point');
|
|
}
|
|
|
|
const transport = new StdioClientTransport({
|
|
command: process.execPath,
|
|
args: [serverEntry],
|
|
cwd: consumer,
|
|
});
|
|
const client = new Client({
|
|
name: 'node-repl-package-smoke',
|
|
version: '0.0.0',
|
|
});
|
|
try {
|
|
await client.connect(transport);
|
|
const tools = await client.listTools();
|
|
const names = tools.tools.map((tool) => tool.name).sort();
|
|
if (
|
|
names.join(',') !==
|
|
'node_repl,node_repl_add_node_module_dir,node_repl_reset'
|
|
) {
|
|
throw new Error(`unexpected MCP tools: ${names.join(',')}`);
|
|
}
|
|
|
|
await client.callTool({
|
|
name: 'node_repl',
|
|
arguments: { code: 'const releaseValue = 40;' },
|
|
});
|
|
const persisted = await client.callTool({
|
|
name: 'node_repl',
|
|
arguments: { code: 'nodeRepl.write(String(releaseValue + 2));' },
|
|
});
|
|
if (!textOf(persisted).includes('42')) {
|
|
throw new Error('packed MCP server did not preserve cell bindings');
|
|
}
|
|
|
|
await client.callTool({ name: 'node_repl_reset', arguments: {} });
|
|
const reset = await client.callTool({
|
|
name: 'node_repl',
|
|
arguments: { code: 'nodeRepl.write(typeof releaseValue);' },
|
|
});
|
|
if (!textOf(reset).includes('undefined')) {
|
|
throw new Error('packed MCP server did not clear bindings on reset');
|
|
}
|
|
} finally {
|
|
await client.close();
|
|
}
|
|
} finally {
|
|
rmSync(consumer, { recursive: true, force: true });
|
|
}
|
|
|
|
process.stdout.write(
|
|
`${JSON.stringify({
|
|
name: packed.name,
|
|
version: packed.version,
|
|
tarball,
|
|
integrity: packed.integrity,
|
|
fileCount: packed.files.length,
|
|
})}\n`,
|
|
);
|