Skip to content

Commit 42d3513

Browse files
committed
feat: auto-generate platform package manifests for CLI, MCP, and HTTP binaries
1 parent 0fe9697 commit 42d3513

File tree

4 files changed

+343
-108
lines changed

4 files changed

+343
-108
lines changed

scripts/copy-rust-binaries.mjs

Lines changed: 126 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,13 @@
1010
* node scripts/copy-rust-binaries.mjs --all # Copy all platforms (requires cross-compilation)
1111
*/
1212
import { promises as fs } from 'node:fs';
13-
import os from 'node:os';
1413
import path from 'node:path';
1514
import { fileURLToPath } from 'node:url';
1615

1716
const __filename = fileURLToPath(import.meta.url);
1817
const __dirname = path.dirname(__filename);
1918
const ROOT = path.resolve(__dirname, '..');
19+
const REPOSITORY_URL = 'https://github.com/codervisor/lean-spec.git';
2020

2121
// Platform mapping
2222
const PLATFORM_MAP = {
@@ -25,14 +25,53 @@ const PLATFORM_MAP = {
2525
win32: { x64: 'windows-x64', arm64: 'windows-arm64' }
2626
};
2727

28+
const PLATFORM_INFO = {
29+
'darwin-x64': { os: 'darwin', cpu: 'x64', label: 'macOS x64' },
30+
'darwin-arm64': { os: 'darwin', cpu: 'arm64', label: 'macOS ARM64' },
31+
'linux-x64': { os: 'linux', cpu: 'x64', label: 'Linux x64' },
32+
'linux-arm64': { os: 'linux', cpu: 'arm64', label: 'Linux ARM64' },
33+
'windows-x64': { os: 'win32', cpu: 'x64', label: 'Windows x64' }
34+
};
35+
2836
// All platforms for --all flag
29-
const ALL_PLATFORMS = [
30-
'darwin-x64',
31-
'darwin-arm64',
32-
'linux-x64',
33-
'linux-arm64',
34-
'windows-x64',
35-
];
37+
const ALL_PLATFORMS = Object.keys(PLATFORM_INFO);
38+
39+
const BINARY_CONFIG = {
40+
'lean-spec': {
41+
packagePath: 'cli',
42+
packagePrefix: '@leanspec/cli',
43+
description: 'LeanSpec CLI binary'
44+
},
45+
'leanspec-mcp': {
46+
packagePath: 'mcp',
47+
packagePrefix: '@leanspec/mcp',
48+
description: 'LeanSpec MCP server binary'
49+
},
50+
'leanspec-http': {
51+
packagePath: 'http-server',
52+
packagePrefix: '@leanspec/http',
53+
description: 'LeanSpec HTTP server binary'
54+
}
55+
};
56+
57+
async function resolveTargetVersion() {
58+
const rootPackagePath = path.join(ROOT, 'package.json');
59+
const rootPackage = JSON.parse(await fs.readFile(rootPackagePath, 'utf-8'));
60+
61+
if (rootPackage.version) {
62+
return rootPackage.version;
63+
}
64+
65+
const cliPackagePath = path.join(ROOT, 'packages', 'cli', 'package.json');
66+
const cliPackage = JSON.parse(await fs.readFile(cliPackagePath, 'utf-8'));
67+
68+
if (!cliPackage.version) {
69+
throw new Error('Unable to resolve version from root or packages/cli/package.json');
70+
}
71+
72+
console.warn('⚠️ Root package.json missing version; using packages/cli/package.json version.');
73+
return cliPackage.version;
74+
}
3675

3776
function getCurrentPlatform() {
3877
const platform = process.platform;
@@ -46,6 +85,60 @@ function getCurrentPlatform() {
4685
return platformKey;
4786
}
4887

88+
function getPlatformInfo(platformKey) {
89+
const info = PLATFORM_INFO[platformKey];
90+
91+
if (!info) {
92+
throw new Error(`Unknown platform key: ${platformKey}`);
93+
}
94+
95+
return info;
96+
}
97+
98+
function getBinaryFileName(binaryName, platformKey) {
99+
return platformKey.startsWith('windows-') ? `${binaryName}.exe` : binaryName;
100+
}
101+
102+
async function ensurePackageJson({
103+
destDir,
104+
platformKey,
105+
binaryName,
106+
packagePrefix,
107+
description,
108+
version,
109+
}) {
110+
const packageJsonPath = path.join(destDir, 'package.json');
111+
const platformInfo = getPlatformInfo(platformKey);
112+
113+
try {
114+
await fs.access(packageJsonPath);
115+
return;
116+
} catch (e) {
117+
// Missing manifest; create below
118+
}
119+
120+
const binaryFileName = getBinaryFileName(binaryName, platformKey);
121+
const packageName = `${packagePrefix}-${platformKey}`;
122+
123+
const packageJson = {
124+
name: packageName,
125+
version,
126+
description: `${description} for ${platformInfo.label}`,
127+
os: [platformInfo.os],
128+
cpu: [platformInfo.cpu],
129+
main: binaryFileName,
130+
files: [binaryFileName],
131+
repository: {
132+
type: 'git',
133+
url: REPOSITORY_URL
134+
},
135+
license: 'MIT'
136+
};
137+
138+
await fs.writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2) + '\n');
139+
console.log(`🆕 Created ${packageName} package.json`);
140+
}
141+
49142
async function killProcessesUsingBinary(binaryPath) {
50143
if (process.platform === 'win32') {
51144
// Windows: use handle.exe or just try to copy
@@ -82,24 +175,18 @@ async function killProcessesUsingBinary(binaryPath) {
82175
}
83176
}
84177

85-
async function copyBinary(binaryName, platformKey) {
86-
const isWindows = platformKey.startsWith('windows-');
87-
const sourceExt = isWindows ? '.exe' : '';
88-
const sourcePath = path.join(ROOT, 'rust', 'target', 'release', `${binaryName}${sourceExt}`);
89-
90-
// Determine destination based on binary name
91-
let packagePath;
92-
if (binaryName === 'lean-spec') {
93-
packagePath = 'cli';
94-
} else if (binaryName === 'leanspec-mcp') {
95-
packagePath = 'mcp';
96-
} else if (binaryName === 'leanspec-http') {
97-
packagePath = 'http-server';
98-
} else {
178+
async function copyBinary(binaryName, platformKey, version) {
179+
const config = BINARY_CONFIG[binaryName];
180+
181+
if (!config) {
99182
throw new Error(`Unknown binary: ${binaryName}`);
100183
}
101-
const destDir = path.join(ROOT, 'packages', packagePath, 'binaries', platformKey);
102-
const destPath = path.join(destDir, binaryName + sourceExt);
184+
185+
const isWindows = platformKey.startsWith('windows-');
186+
const binaryFileName = getBinaryFileName(binaryName, platformKey);
187+
const sourcePath = path.join(ROOT, 'rust', 'target', 'release', binaryFileName);
188+
const destDir = path.join(ROOT, 'packages', config.packagePath, 'binaries', platformKey);
189+
const destPath = path.join(destDir, binaryFileName);
103190

104191
// Check if source exists
105192
try {
@@ -112,6 +199,16 @@ async function copyBinary(binaryName, platformKey) {
112199
// Ensure destination directory exists
113200
await fs.mkdir(destDir, { recursive: true });
114201

202+
// Ensure platform package manifest exists
203+
await ensurePackageJson({
204+
destDir,
205+
platformKey,
206+
binaryName,
207+
packagePrefix: config.packagePrefix,
208+
description: config.description,
209+
version,
210+
});
211+
115212
// Kill any processes using the destination binary
116213
try {
117214
await fs.access(destPath);
@@ -128,14 +225,16 @@ async function copyBinary(binaryName, platformKey) {
128225
await fs.chmod(destPath, 0o755);
129226
}
130227

131-
console.log(`✅ Copied ${binaryName} to ${packagePath}/binaries/${platformKey}/`);
228+
console.log(`✅ Copied ${binaryName} to ${config.packagePath}/binaries/${platformKey}/`);
132229
return true;
133230
}
134231

135232
async function main() {
136233
const args = process.argv.slice(2);
137234
const copyAll = args.includes('--all');
138235

236+
const rootVersion = await resolveTargetVersion();
237+
139238
console.log('🔧 Copying Rust binaries...\n');
140239

141240
const binaries = ['lean-spec', 'leanspec-mcp', 'leanspec-http'];
@@ -146,15 +245,15 @@ async function main() {
146245
for (const platformKey of ALL_PLATFORMS) {
147246
console.log(`\nPlatform: ${platformKey}`);
148247
for (const binary of binaries) {
149-
await copyBinary(binary, platformKey);
248+
await copyBinary(binary, platformKey, rootVersion);
150249
}
151250
}
152251
} else {
153252
const currentPlatform = getCurrentPlatform();
154253
console.log(`📦 Copying for current platform: ${currentPlatform}\n`);
155254

156255
for (const binary of binaries) {
157-
await copyBinary(binary, currentPlatform);
256+
await copyBinary(binary, currentPlatform, rootVersion);
158257
}
159258
}
160259

0 commit comments

Comments
 (0)