fix: support standalone binary CLI compilation
Some checks are pending
CI / build (macos-latest) (push) Waiting to run
CI / build (ubuntu-latest) (push) Waiting to run
CI / build (windows-latest) (push) Waiting to run

This commit is contained in:
Peter Steinberger 2026-05-04 23:09:04 +01:00
parent 6ed98602ef
commit 026eb28cf4
No known key found for this signature in database
4 changed files with 142 additions and 2 deletions

View File

@ -4,6 +4,16 @@
- Nothing yet.
## [0.10.1] - 2026-05-04
### CLI
- Fix Bun-compiled standalone binaries so `generate-cli --compile` can compile generated CLIs from empty directories by staging the matching published `mcporter` package dependencies when no local package tree is available.
### Tests
- Add an opt-in standalone Bun release-binary smoke for the empty-directory generated CLI compile path.
## [0.10.0] - 2026-05-04
### CLI

View File

@ -1,6 +1,6 @@
{
"name": "mcporter",
"version": "0.10.0",
"version": "0.10.1",
"description": "TypeScript runtime and CLI for connecting to configured Model Context Protocol servers.",
"keywords": [
"cli",

View File

@ -5,6 +5,7 @@ import { createRequire } from 'node:module';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import type { RolldownPlugin } from 'rolldown';
import { MCPORTER_VERSION } from '../../runtime.js';
import { markExecutable, safeCopyFile } from './fs-helpers.js';
import { verifyBunAvailable } from './runtime.js';
@ -110,7 +111,7 @@ async function bundleWithBun({
args.push('--minify');
}
await new Promise<void>((resolve, reject) => {
execFile(bunBin, args, { cwd: packageRoot, env: process.env }, (error) => {
execFile(bunBin, args, { cwd: stagingDir, env: process.env }, (error) => {
if (error) {
reject(error);
return;
@ -255,6 +256,62 @@ async function ensureBundlerDeps(stagingDir: string): Promise<void> {
await linkOrCopyDependency(sourceDir, target);
})
);
const missing = await findMissingBundlerDeps(stagingDir);
if (missing.length > 0) {
await installPublishedBundlerDeps(stagingDir);
}
}
async function findMissingBundlerDeps(stagingDir: string): Promise<string[]> {
const missing: string[] = [];
for (const specifier of BUNDLED_DEPENDENCIES) {
const pkgPath = path.join(stagingDir, 'node_modules', specifier, 'package.json');
try {
await fs.access(pkgPath);
} catch {
missing.push(specifier);
}
}
return missing;
}
async function installPublishedBundlerDeps(stagingDir: string): Promise<void> {
const installSpec = process.env.MCPORTER_BUNDLER_DEP_PACKAGE ?? MCPORTER_VERSION;
if (installSpec === '0.0.0-dev') {
throw new Error(
'Unable to resolve generated-CLI bundler dependencies from this standalone mcporter binary. Install mcporter via npm/Homebrew or publish the matching mcporter package before using --compile.'
);
}
await fs.writeFile(
path.join(stagingDir, 'package.json'),
JSON.stringify({ private: true, type: 'module', dependencies: { mcporter: installSpec } }, null, 2),
'utf8'
);
await new Promise<void>((resolve, reject) => {
execFile(
'npm',
['install', '--ignore-scripts', '--no-audit', '--no-fund', '--min-release-age=0'],
{ cwd: stagingDir, env: process.env },
(error) => {
if (error) {
reject(
new Error(
`Unable to install ${formatMcporterInstallSpec(installSpec)} dependencies needed for Bun compilation from this standalone binary. Install mcporter via npm/Homebrew, or ensure npm can reach the registry.\n\n${error.message}`
)
);
return;
}
resolve();
}
);
});
}
function formatMcporterInstallSpec(installSpec: string): string {
if (installSpec === MCPORTER_VERSION) {
return `mcporter@${MCPORTER_VERSION}`;
}
return `mcporter from ${installSpec}`;
}
function resolveDependencyDirectory(specifier: (typeof BUNDLED_DEPENDENCIES)[number]): string | undefined {

View File

@ -863,4 +863,77 @@ await new Promise((resolve) => { transport.onclose = resolve; });
await fs.rm(tempDir, { recursive: true, force: true }).catch(() => {});
await fs.rm(callerCwd, { recursive: true, force: true }).catch(() => {});
}, 30_000);
it('compiles a generated CLI from the standalone Bun release binary in an empty directory', async () => {
if (process.env.MCPORTER_STANDALONE_BINARY_TEST !== '1') {
console.warn('set MCPORTER_STANDALONE_BINARY_TEST=1 to run standalone Bun release binary smoke');
return;
}
if (!(await ensureBunSupport('standalone Bun release binary smoke'))) {
return;
}
await new Promise<void>((resolve, reject) => {
execFile(PNPM_COMMAND, pnpmArgs(['build:bun']), { cwd: process.cwd(), env: process.env }, (error) => {
if (error) {
reject(error);
return;
}
resolve();
});
});
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'mcporter-standalone-bun-'));
const binaryPath = path.join(tempDir, 'context7-cli');
const mcporterBinary = path.join(process.cwd(), 'dist-bun', 'mcporter');
const packedTarball = await new Promise<string>((resolve, reject) => {
execFile(
'npm',
['pack', '--ignore-scripts', '--pack-destination', tempDir],
{ cwd: process.cwd(), env: process.env },
(error, stdout, stderr) => {
if (error) {
reject(new Error(`${error.message}\nSTDOUT:\n${stdout}\nSTDERR:\n${stderr}`));
return;
}
resolve(path.join(tempDir, stdout.trim().split('\n').at(-1) ?? ''));
}
);
});
await new Promise<void>((resolve, reject) => {
execFile(
mcporterBinary,
['generate-cli', '--command', baseUrl.toString(), '--compile', binaryPath],
{
cwd: tempDir,
env: {
...process.env,
MCPORTER_BUNDLER_DEP_PACKAGE: packedTarball,
MCPORTER_NO_FORCE_EXIT: '1',
},
},
(error, stdout, stderr) => {
if (error) {
reject(new Error(`${error.message}\nSTDOUT:\n${stdout}\nSTDERR:\n${stderr}`));
return;
}
resolve();
}
);
});
const helpOutput = await new Promise<{ stdout: string; stderr: string }>((resolve, reject) => {
execFile(binaryPath, [], { env: process.env }, (error, stdout, stderr) => {
if (error) {
reject(error);
return;
}
resolve({ stdout, stderr });
});
});
expect(helpOutput.stdout).toMatch(/Usage: .+ <command> \[options]/);
expect(helpOutput.stdout).toContain('ping - Simple health check');
await fs.rm(tempDir, { recursive: true, force: true }).catch(() => {});
}, 90_000);
});