- Implement tests for random ID generation, ensuring preference for crypto.randomUUID. - Create tests for runtime context capabilities, validating the injection of enabled skill capabilities. - Add tests for skill capability parsing, including classification and command example extraction. - Introduce tests for the skill planner, verifying tool call planning based on user requests and attachment requirements. - Establish tests for UV setup, ensuring proper handling of Python installation scenarios and environment checks.
308 lines
9.7 KiB
JavaScript
308 lines
9.7 KiB
JavaScript
/**
|
|
* after-pack.cjs
|
|
*
|
|
* electron-builder afterPack hook for zn-ai
|
|
*
|
|
* This hook runs AFTER electron-builder finishes packing.
|
|
* It handles:
|
|
* 1. Copying and bundling electron/scripts/ directory
|
|
* 2. Ensuring required dependencies (playwright, chromium-bidi, bytenode) are included
|
|
* 3. Copying bundled OpenClaw runtime dependencies that electron-builder may skip
|
|
* 4. Cleaning up unnecessary development files to reduce package size
|
|
*/
|
|
|
|
const fs = require('fs-extra');
|
|
const path = require('path');
|
|
const esbuild = require('esbuild');
|
|
|
|
const ARCH_NAME_MAP = {
|
|
0: 'ia32',
|
|
1: 'x64',
|
|
2: 'armv7l',
|
|
3: 'arm64',
|
|
4: 'universal',
|
|
};
|
|
|
|
const REQUIRED_RUNTIME_BINARIES = {
|
|
win32: ['uv.exe', 'node.exe'],
|
|
darwin: ['uv'],
|
|
linux: ['uv'],
|
|
};
|
|
|
|
function normalizePackArch(arch) {
|
|
if (typeof arch === 'string') {
|
|
return arch;
|
|
}
|
|
|
|
return ARCH_NAME_MAP[arch] || String(arch);
|
|
}
|
|
|
|
function normalizePlatformName(electronPlatformName) {
|
|
switch (electronPlatformName) {
|
|
case 'win':
|
|
return 'win32';
|
|
case 'mac':
|
|
return 'darwin';
|
|
default:
|
|
return electronPlatformName;
|
|
}
|
|
}
|
|
|
|
function resolveBundledRuntimeTarget(context) {
|
|
const platform = normalizePlatformName(context.electronPlatformName);
|
|
const arch = normalizePackArch(context.arch);
|
|
return `${platform}-${arch}`;
|
|
}
|
|
|
|
async function copyBundledRuntimeBinaries(context, overrides = {}) {
|
|
const target = resolveBundledRuntimeTarget(context);
|
|
const platform = normalizePlatformName(context.electronPlatformName);
|
|
const sourceRoot = overrides.sourceRoot || path.join(__dirname, '..', 'resources', 'bin', target);
|
|
const resourcesDir = overrides.resourcesDir || resolveResourcesDir(context);
|
|
const destRoot = overrides.destRoot || path.join(resourcesDir, 'bin');
|
|
const requiredFiles = overrides.requiredFiles || REQUIRED_RUNTIME_BINARIES[platform] || [];
|
|
|
|
if (!(await fs.pathExists(sourceRoot))) {
|
|
throw new Error(
|
|
`[after-pack] Missing bundled runtime directory for ${target}: ${sourceRoot}. ` +
|
|
`Run the bundled runtime preparation step before packaging.`,
|
|
);
|
|
}
|
|
|
|
await fs.ensureDir(destRoot);
|
|
|
|
const entries = await fs.readdir(sourceRoot, { withFileTypes: true });
|
|
for (const entry of entries) {
|
|
if (!entry.isFile()) {
|
|
continue;
|
|
}
|
|
|
|
await fs.copy(path.join(sourceRoot, entry.name), path.join(destRoot, entry.name), {
|
|
overwrite: true,
|
|
});
|
|
}
|
|
|
|
const missing = requiredFiles.filter((fileName) => !fs.existsSync(path.join(destRoot, fileName)));
|
|
if (missing.length > 0) {
|
|
throw new Error(
|
|
`[after-pack] Missing required bundled runtime binaries for ${target}: ${missing.join(', ')}.`,
|
|
);
|
|
}
|
|
|
|
console.log(`[after-pack] Copied bundled runtime binaries for ${target} -> ${destRoot}`);
|
|
}
|
|
|
|
/**
|
|
* Remove development artifacts from a directory (recursive).
|
|
* Removes: test directories, TypeScript definitions, source maps, docs, etc.
|
|
*/
|
|
function cleanupUnnecessaryFiles(dir) {
|
|
let removedCount = 0;
|
|
|
|
const REMOVE_DIRS = new Set([
|
|
'test', 'tests', '__tests__', '.github', 'docs', 'examples', 'example',
|
|
]);
|
|
const REMOVE_FILE_EXTS = ['.d.ts', '.d.ts.map', '.js.map', '.mjs.map', '.ts.map', '.markdown'];
|
|
const REMOVE_FILE_NAMES = new Set([
|
|
'.DS_Store', 'README.md', 'CHANGELOG.md', 'LICENSE.md', 'CONTRIBUTING.md',
|
|
'tsconfig.json', '.npmignore', '.eslintrc', '.prettierrc', '.editorconfig',
|
|
]);
|
|
|
|
function walk(currentDir) {
|
|
let entries;
|
|
try {
|
|
entries = fs.readdirSync(currentDir, { withFileTypes: true });
|
|
} catch {
|
|
return;
|
|
}
|
|
|
|
for (const entry of entries) {
|
|
const fullPath = path.join(currentDir, entry.name);
|
|
|
|
if (entry.isDirectory()) {
|
|
if (REMOVE_DIRS.has(entry.name)) {
|
|
try {
|
|
fs.rmSync(fullPath, { recursive: true, force: true });
|
|
removedCount++;
|
|
} catch { /* ignore */ }
|
|
} else {
|
|
walk(fullPath);
|
|
}
|
|
} else if (entry.isFile()) {
|
|
const name = entry.name;
|
|
if (REMOVE_FILE_NAMES.has(name) || REMOVE_FILE_EXTS.some(e => name.endsWith(e))) {
|
|
try {
|
|
fs.rmSync(fullPath, { force: true });
|
|
removedCount++;
|
|
} catch { /* ignore */ }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
walk(dir);
|
|
return removedCount;
|
|
}
|
|
|
|
/**
|
|
* Clean up platform-specific native packages (optional).
|
|
* Currently not needed for zn-ai, but kept as a placeholder.
|
|
*/
|
|
function cleanupNativePlatformPackages(nodeModulesDir, platform, arch) {
|
|
// No platform-specific native packages identified for zn-ai yet
|
|
return 0;
|
|
}
|
|
|
|
function resolveResourcesDir(context) {
|
|
if (context.electronPlatformName === 'darwin') {
|
|
if (context.appOutDir.endsWith('.app')) {
|
|
return path.join(context.appOutDir, 'Contents', 'Resources');
|
|
}
|
|
|
|
const appName = context.packager?.appInfo?.productFilename || 'NIANXX';
|
|
return path.join(context.appOutDir, `${appName}.app`, 'Contents', 'Resources');
|
|
}
|
|
|
|
return path.join(context.appOutDir, 'resources');
|
|
}
|
|
|
|
async function copyBundledOpenClaw(context) {
|
|
const srcRoot = path.join(__dirname, '..', 'build', 'openclaw');
|
|
if (!(await fs.pathExists(srcRoot))) {
|
|
console.warn('[after-pack] build/openclaw not found. Run bundle-openclaw first.');
|
|
return;
|
|
}
|
|
|
|
const resourcesDir = resolveResourcesDir(context);
|
|
const destRoot = path.join(resourcesDir, 'openclaw');
|
|
const srcNodeModules = path.join(srcRoot, 'node_modules');
|
|
const destNodeModules = path.join(destRoot, 'node_modules');
|
|
|
|
await fs.ensureDir(resourcesDir);
|
|
|
|
if (!(await fs.pathExists(destRoot))) {
|
|
await fs.copy(srcRoot, destRoot, {
|
|
filter: (src) => {
|
|
const relative = path.relative(srcRoot, src);
|
|
if (!relative) return true;
|
|
return relative.split(path.sep)[0] !== 'node_modules';
|
|
},
|
|
});
|
|
console.log('[after-pack] Copied bundled OpenClaw runtime root.');
|
|
}
|
|
|
|
if (!(await fs.pathExists(srcNodeModules))) {
|
|
console.warn('[after-pack] build/openclaw/node_modules not found. OpenClaw runtime will be incomplete.');
|
|
return;
|
|
}
|
|
|
|
if (!(await fs.pathExists(destNodeModules))) {
|
|
await fs.copy(srcNodeModules, destNodeModules);
|
|
console.log('[after-pack] Copied bundled OpenClaw node_modules.');
|
|
}
|
|
}
|
|
|
|
module.exports = async function afterPack(context) {
|
|
const { appOutDir, electronPlatformName: platform, arch } = context;
|
|
const resourcesDir = resolveResourcesDir(context);
|
|
|
|
console.log(`Running afterPack hook for ${platform}-${arch}`);
|
|
console.log(`App output directory: ${appOutDir}`);
|
|
|
|
await copyBundledRuntimeBinaries(context, { resourcesDir });
|
|
|
|
// 1. Handle electron/scripts/ directory
|
|
const scriptsSrc = path.join(__dirname, '..', 'electron/scripts');
|
|
const scriptsDest = path.join(resourcesDir, 'scripts');
|
|
|
|
if (await fs.pathExists(scriptsSrc)) {
|
|
await fs.ensureDir(scriptsDest);
|
|
const files = await fs.readdir(scriptsSrc);
|
|
|
|
for (const file of files) {
|
|
const srcFile = path.join(scriptsSrc, file);
|
|
const destFile = path.join(scriptsDest, file);
|
|
|
|
if (file.endsWith('.js')) {
|
|
// Bundle JavaScript files with esbuild
|
|
try {
|
|
await esbuild.build({
|
|
entryPoints: [srcFile],
|
|
outfile: destFile,
|
|
bundle: true,
|
|
platform: 'node',
|
|
target: 'node24', // Adjust based on Electron version
|
|
external: ['electron'], // Exclude electron from bundling
|
|
format: 'cjs',
|
|
});
|
|
console.log(`Bundled: ${file}`);
|
|
} catch (error) {
|
|
console.error(`Failed to bundle ${file}:`, error);
|
|
// Fallback to copying the file
|
|
await fs.copy(srcFile, destFile);
|
|
}
|
|
} else {
|
|
// Copy other files as-is
|
|
await fs.copy(srcFile, destFile);
|
|
console.log(`Copied: ${file}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
await copyBundledOpenClaw(context);
|
|
|
|
// 2. Ensure required dependencies are included
|
|
// electron-builder may exclude some dependencies due to .gitignore rules
|
|
|
|
const ensureDependency = async (depName) => {
|
|
const src = path.join(__dirname, '..', 'node_modules', depName);
|
|
const dest = path.join(resourcesDir, 'node_modules', depName);
|
|
|
|
if (await fs.pathExists(src)) {
|
|
await fs.ensureDir(path.dirname(dest));
|
|
if (!(await fs.pathExists(dest))) {
|
|
await fs.copy(src, dest);
|
|
console.log(`Copied dependency: ${depName}`);
|
|
}
|
|
}
|
|
};
|
|
|
|
// List of dependencies that need to be explicitly copied
|
|
const requiredDeps = ['playwright', 'playwright-core', 'chromium-bidi', 'bytenode'];
|
|
|
|
for (const dep of requiredDeps) {
|
|
await ensureDependency(dep);
|
|
}
|
|
|
|
// 3. Clean up unnecessary development files from node_modules (skip if SKIP_AFTERPACK_CLEANUP is set)
|
|
if (!process.env.SKIP_AFTERPACK_CLEANUP) {
|
|
const nodeModulesDest = path.join(resourcesDir, 'node_modules');
|
|
if (await fs.pathExists(nodeModulesDest)) {
|
|
console.log('Cleaning up development files in node_modules...');
|
|
const removed = cleanupUnnecessaryFiles(nodeModulesDest);
|
|
console.log(`Removed ${removed} unnecessary files/directories from node_modules.`);
|
|
}
|
|
|
|
// 4. Clean up unnecessary files in scripts directory
|
|
if (await fs.pathExists(scriptsDest)) {
|
|
console.log('Cleaning up development files in scripts directory...');
|
|
const removedScripts = cleanupUnnecessaryFiles(scriptsDest);
|
|
console.log(`Removed ${removedScripts} unnecessary files/directories from scripts.`);
|
|
}
|
|
} else {
|
|
console.log('Skipping afterPack cleanup (SKIP_AFTERPACK_CLEANUP is set)');
|
|
}
|
|
|
|
// 5. Optional: platform-specific native package cleanup
|
|
// cleanupNativePlatformPackages(nodeModulesDest, platform, arch);
|
|
|
|
console.log('afterPack hook completed successfully');
|
|
};
|
|
|
|
module.exports.__test__ = {
|
|
normalizePackArch,
|
|
normalizePlatformName,
|
|
resolveBundledRuntimeTarget,
|
|
copyBundledRuntimeBinaries,
|
|
};
|