mirror of
https://github.com/ValueCell-ai/ClawX.git
synced 2026-08-14 08:53:09 +00:00
Compare commits
16
Commits
v0.5.3
...
v0.4.0-alpha.8
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
464fd1f08a | ||
|
|
9b130880c3 | ||
|
|
899ea52175 | ||
|
|
38a7a4cebd | ||
|
|
153fb5dd86 | ||
|
|
38f6566fda | ||
|
|
788e77da79 | ||
|
|
c024ef9392 | ||
|
|
ccdb4e739d | ||
|
|
972aca6e2c | ||
|
|
299d4c89f9 | ||
|
|
ea8799435f | ||
|
|
213d34bfcf | ||
|
|
420fa92d6e | ||
|
|
f88be09846 | ||
|
|
b0daa6139f |
+10
-8
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "clawx",
|
||||
"version": "0.3.11",
|
||||
"version": "0.4.0-alpha.8",
|
||||
"pnpm": {
|
||||
"onlyBuiltDependencies": [
|
||||
"@discordjs/opus",
|
||||
@@ -36,7 +36,7 @@
|
||||
"predev": "node scripts/generate-ext-bridge.mjs && zx scripts/prepare-preinstalled-skills-dev.mjs",
|
||||
"dev": "vite",
|
||||
"ext:bridge": "node scripts/generate-ext-bridge.mjs",
|
||||
"build": "node scripts/generate-ext-bridge.mjs && pnpm run build:vite && zx scripts/bundle-openclaw.mjs && zx scripts/bundle-openclaw-plugins.mjs && zx scripts/bundle-preinstalled-skills.mjs && electron-builder",
|
||||
"build": "node scripts/generate-ext-bridge.mjs && pnpm run build:vite && zx scripts/bundle-openclaw.mjs && zx scripts/bundle-openclaw-plugins.mjs && zx scripts/bundle-preinstalled-skills.mjs && node scripts/run-electron-builder.mjs",
|
||||
"build:vite": "node --max-old-space-size=6144 ./node_modules/vite/bin/vite.js build",
|
||||
"bundle:openclaw-plugins": "zx scripts/bundle-openclaw-plugins.mjs",
|
||||
"bundle:preinstalled-skills": "zx scripts/bundle-preinstalled-skills.mjs",
|
||||
@@ -60,15 +60,17 @@
|
||||
"prep:win-binaries": "pnpm run uv:download:win && pnpm run node:download:win",
|
||||
"icons": "zx scripts/generate-icons.mjs",
|
||||
"package": "node scripts/generate-ext-bridge.mjs && pnpm run build:vite && zx scripts/bundle-openclaw.mjs && zx scripts/bundle-openclaw-plugins.mjs && zx scripts/bundle-preinstalled-skills.mjs",
|
||||
"package:mac": "pnpm run package && electron-builder --mac --publish never",
|
||||
"package:mac:local": "SKIP_PREINSTALLED_SKILLS=1 pnpm run package && electron-builder --mac --publish never",
|
||||
"package:win": "pnpm run prep:win-binaries && pnpm run package && electron-builder --win --publish never",
|
||||
"package:linux": "pnpm run package && electron-builder --linux --publish never",
|
||||
"release": "pnpm run uv:download && pnpm run package && electron-builder --publish always",
|
||||
"package:mac": "pnpm run package && node scripts/run-electron-builder.mjs --mac --publish never",
|
||||
"package:mac:local": "SKIP_PREINSTALLED_SKILLS=1 pnpm run package && node scripts/run-electron-builder.mjs --mac --publish never",
|
||||
"package:win": "pnpm run prep:win-binaries && pnpm run package && node scripts/run-electron-builder.mjs --win --publish never",
|
||||
"package:linux": "pnpm run package && node scripts/run-electron-builder.mjs --linux --publish never",
|
||||
"release": "pnpm run uv:download && pnpm run package && node scripts/run-electron-builder.mjs --publish always",
|
||||
"version": "node scripts/assert-release-version.mjs",
|
||||
"version:patch": "pnpm version patch",
|
||||
"version:minor": "pnpm version minor",
|
||||
"version:major": "pnpm version major",
|
||||
"version:preminor-alpha": "pnpm version preminor --preid=alpha",
|
||||
"version:prerelease-alpha": "pnpm version prerelease --preid=alpha",
|
||||
"version:prerelease-beta": "pnpm version prerelease --preid=beta",
|
||||
"release:validate": "node scripts/assert-tag-matches-package.mjs",
|
||||
"postversion": "git push && git push --tags"
|
||||
@@ -177,4 +179,4 @@
|
||||
"zx": "^8.8.5"
|
||||
},
|
||||
"packageManager": "pnpm@10.31.0+sha512.e3927388bfaa8078ceb79b748ffc1e8274e84d75163e67bc22e06c0d3aed43dd153151cbf11d7f8301ff4acb98c68bdc5cadf6989532801ffafe3b3e4a63c268"
|
||||
}
|
||||
}
|
||||
+128
-27
@@ -19,7 +19,7 @@
|
||||
* @mariozechner/clipboard).
|
||||
*/
|
||||
|
||||
const { cpSync, existsSync, readdirSync, rmSync, statSync, mkdirSync, realpathSync } = require('fs');
|
||||
const { cpSync, existsSync, readdirSync, rmSync, statSync, mkdirSync, realpathSync, readFileSync, writeFileSync } = require('fs');
|
||||
const { join, dirname, basename, relative } = require('path');
|
||||
|
||||
// On Windows, paths in pnpm's virtual store can exceed the default MAX_PATH
|
||||
@@ -40,6 +40,26 @@ function resolveArch(archEnum) {
|
||||
return ARCH_MAP[archEnum] || 'x64';
|
||||
}
|
||||
|
||||
function readJsonSafe(filePath) {
|
||||
try {
|
||||
return JSON.parse(readFileSync(normWin(filePath), 'utf8'));
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function listPackageDeps(pkgJson) {
|
||||
return Object.keys({
|
||||
...(pkgJson?.dependencies && typeof pkgJson.dependencies === 'object' ? pkgJson.dependencies : {}),
|
||||
...(pkgJson?.optionalDependencies && typeof pkgJson.optionalDependencies === 'object' ? pkgJson.optionalDependencies : {}),
|
||||
}).sort((a, b) => a.localeCompare(b));
|
||||
}
|
||||
|
||||
function readInstalledPackageVersion(packageDir) {
|
||||
const pkg = readJsonSafe(join(packageDir, 'package.json'));
|
||||
return typeof pkg?.version === 'string' ? pkg.version.trim() : null;
|
||||
}
|
||||
|
||||
// ── General cleanup ──────────────────────────────────────────────────────────
|
||||
|
||||
function cleanupUnnecessaryFiles(dir) {
|
||||
@@ -48,7 +68,17 @@ function cleanupUnnecessaryFiles(dir) {
|
||||
const REMOVE_DIRS = new Set([
|
||||
'test', 'tests', '__tests__', '.github', 'examples', 'example',
|
||||
]);
|
||||
const REMOVE_FILE_EXTS = ['.d.ts', '.d.ts.map', '.js.map', '.mjs.map', '.ts.map', '.markdown'];
|
||||
// .d.mts / .d.cts are TypeScript declaration files for ESM/CJS dual-package
|
||||
// builds. They are useless at runtime but show up in huge volumes from
|
||||
// typed packages (e.g. typebox), and inflate the per-process file count
|
||||
// that codesign opens during macOS signing → EMFILE.
|
||||
const REMOVE_FILE_EXTS = [
|
||||
'.d.ts', '.d.ts.map',
|
||||
'.d.mts', '.d.mts.map',
|
||||
'.d.cts', '.d.cts.map',
|
||||
'.js.map', '.mjs.map', '.cjs.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',
|
||||
@@ -607,50 +637,121 @@ exports.default = async function afterPack(context) {
|
||||
// the top-level node_modules/ as well.
|
||||
const buildExtDir = join(__dirname, '..', 'build', 'openclaw', 'dist', 'extensions');
|
||||
const packExtDir = join(openclawRoot, 'dist', 'extensions');
|
||||
// ClawX always uses the official @larksuite/openclaw-lark plugin for Feishu.
|
||||
// The built-in openclaw dist/extensions/feishu tree is redundant, and on macOS
|
||||
// its mirrored runtime deps significantly increase codesign file pressure.
|
||||
rmSync(join(packExtDir, 'feishu'), { recursive: true, force: true });
|
||||
if (existsSync(buildExtDir)) {
|
||||
let extNMCount = 0;
|
||||
let mergedPkgCount = 0;
|
||||
let prunedSharedDepCount = 0;
|
||||
for (const extEntry of readdirSync(buildExtDir, { withFileTypes: true })) {
|
||||
if (!extEntry.isDirectory()) continue;
|
||||
if (extEntry.name === 'feishu') continue;
|
||||
|
||||
const srcNM = join(buildExtDir, extEntry.name, 'node_modules');
|
||||
if (!existsSync(srcNM)) continue;
|
||||
|
||||
// Copy to extension's own node_modules (for direct requires from extension code).
|
||||
// electron-builder may leave behind an empty destination directory when it
|
||||
// skips node_modules content via .gitignore filtering, so always replace it.
|
||||
const destExtNM = join(packExtDir, extEntry.name, 'node_modules');
|
||||
const destExtRoot = join(packExtDir, extEntry.name);
|
||||
const destExtNM = join(destExtRoot, 'node_modules');
|
||||
rmSync(destExtNM, { recursive: true, force: true });
|
||||
cpSync(srcNM, destExtNM, { recursive: true });
|
||||
mkdirSync(destExtNM, { recursive: true });
|
||||
extNMCount++;
|
||||
|
||||
// Merge into top-level openclaw/node_modules/ (for shared chunks in dist/)
|
||||
for (const pkgEntry of readdirSync(srcNM, { withFileTypes: true })) {
|
||||
if (!pkgEntry.isDirectory() || pkgEntry.name === '.bin') continue;
|
||||
const srcPkg = join(srcNM, pkgEntry.name);
|
||||
const destPkg = join(dest, pkgEntry.name);
|
||||
if (platform === 'darwin') {
|
||||
const buildPkgPath = join(buildExtDir, extEntry.name, 'package.json');
|
||||
const packPkgPath = join(destExtRoot, 'package.json');
|
||||
const buildPkgJson = readJsonSafe(buildPkgPath) || {};
|
||||
// Deep copy the fallback so mutations to packPkgJson don't bleed back
|
||||
// into buildPkgJson (which we still iterate via listPackageDeps below).
|
||||
const packPkgJson = readJsonSafe(packPkgPath) || JSON.parse(JSON.stringify(buildPkgJson));
|
||||
|
||||
if (pkgEntry.name.startsWith('@')) {
|
||||
// Scoped package — iterate sub-entries
|
||||
for (const scopeEntry of readdirSync(srcPkg, { withFileTypes: true })) {
|
||||
if (!scopeEntry.isDirectory()) continue;
|
||||
const srcScoped = join(srcPkg, scopeEntry.name);
|
||||
const destScoped = join(destPkg, scopeEntry.name);
|
||||
if (!existsSync(destScoped)) {
|
||||
mkdirSync(dirname(destScoped), { recursive: true });
|
||||
cpSync(srcScoped, destScoped, { recursive: true });
|
||||
for (const depName of listPackageDeps(buildPkgJson)) {
|
||||
const srcDepPkg = join(srcNM, ...depName.split('/'));
|
||||
const destDepPkg = join(dest, ...depName.split('/'));
|
||||
if (!existsSync(destDepPkg) && existsSync(srcDepPkg)) {
|
||||
mkdirSync(dirname(destDepPkg), { recursive: true });
|
||||
cpSync(srcDepPkg, destDepPkg, { recursive: true });
|
||||
mergedPkgCount++;
|
||||
}
|
||||
|
||||
// Reuse the top-level openclaw/node_modules copy whenever possible:
|
||||
// - if src is missing, we have no reference version → trust top-level
|
||||
// (Node's module resolution will walk up from
|
||||
// dist/extensions/<ext>/<file>.js to openclaw/node_modules/ anyway).
|
||||
// - if src exists and matches top-level version, we can safely share.
|
||||
// Only force a per-extension local copy when we actually have a
|
||||
// version conflict between the extension's pinned dep and the
|
||||
// top-level shared dep.
|
||||
const srcVersion = existsSync(srcDepPkg) ? readInstalledPackageVersion(srcDepPkg) : null;
|
||||
const destVersion = existsSync(destDepPkg) ? readInstalledPackageVersion(destDepPkg) : null;
|
||||
const canReuseTopLevel = existsSync(destDepPkg) && (
|
||||
!srcVersion || (destVersion && srcVersion === destVersion)
|
||||
);
|
||||
if (canReuseTopLevel) {
|
||||
if (packPkgJson.dependencies && depName in packPkgJson.dependencies) {
|
||||
delete packPkgJson.dependencies[depName];
|
||||
prunedSharedDepCount++;
|
||||
}
|
||||
if (packPkgJson.optionalDependencies && depName in packPkgJson.optionalDependencies) {
|
||||
delete packPkgJson.optionalDependencies[depName];
|
||||
prunedSharedDepCount++;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
const extDepPkg = join(destExtNM, ...depName.split('/'));
|
||||
mkdirSync(dirname(extDepPkg), { recursive: true });
|
||||
if (existsSync(srcDepPkg)) {
|
||||
cpSync(srcDepPkg, extDepPkg, { recursive: true });
|
||||
} else if (existsSync(destDepPkg)) {
|
||||
cpSync(destDepPkg, extDepPkg, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
if (packPkgJson.dependencies && Object.keys(packPkgJson.dependencies).length === 0) {
|
||||
delete packPkgJson.dependencies;
|
||||
}
|
||||
if (packPkgJson.optionalDependencies && Object.keys(packPkgJson.optionalDependencies).length === 0) {
|
||||
delete packPkgJson.optionalDependencies;
|
||||
}
|
||||
writeFileSync(packPkgPath, JSON.stringify(packPkgJson, null, 2) + '\n', 'utf8');
|
||||
} else {
|
||||
for (const pkgEntry of readdirSync(srcNM, { withFileTypes: true })) {
|
||||
if (!pkgEntry.isDirectory() || pkgEntry.name === '.bin') continue;
|
||||
const srcPkg = join(srcNM, pkgEntry.name);
|
||||
const destPkg = join(dest, pkgEntry.name);
|
||||
|
||||
if (pkgEntry.name.startsWith('@')) {
|
||||
// Scoped package — iterate sub-entries
|
||||
for (const scopeEntry of readdirSync(srcPkg, { withFileTypes: true })) {
|
||||
if (!scopeEntry.isDirectory()) continue;
|
||||
const srcScoped = join(srcPkg, scopeEntry.name);
|
||||
const destScoped = join(destPkg, scopeEntry.name);
|
||||
if (!existsSync(destScoped)) {
|
||||
mkdirSync(dirname(destScoped), { recursive: true });
|
||||
cpSync(srcScoped, destScoped, { recursive: true });
|
||||
mergedPkgCount++;
|
||||
}
|
||||
|
||||
const extScoped = join(destExtNM, pkgEntry.name, scopeEntry.name);
|
||||
mkdirSync(dirname(extScoped), { recursive: true });
|
||||
cpSync(srcScoped, extScoped, { recursive: true });
|
||||
}
|
||||
} else {
|
||||
if (!existsSync(destPkg)) {
|
||||
cpSync(srcPkg, destPkg, { recursive: true });
|
||||
mergedPkgCount++;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (!existsSync(destPkg)) {
|
||||
cpSync(srcPkg, destPkg, { recursive: true });
|
||||
mergedPkgCount++;
|
||||
|
||||
const extPkg = join(destExtNM, pkgEntry.name);
|
||||
cpSync(srcPkg, extPkg, { recursive: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (extNMCount > 0) {
|
||||
console.log(`[after-pack] ✅ Copied node_modules for ${extNMCount} built-in extension(s), merged ${mergedPkgCount} packages into top-level.`);
|
||||
console.log(`[after-pack] ✅ Prepared node_modules for ${extNMCount} built-in extension(s), merged ${mergedPkgCount} packages into top-level${prunedSharedDepCount > 0 ? `, pruned ${prunedSharedDepCount} redundant direct deps on macOS` : ''}.`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -56,10 +56,36 @@ function shouldCopyOpenClawPackageEntry(src) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 2. Clean and create output directory
|
||||
if (fs.existsSync(OUTPUT)) {
|
||||
fs.rmSync(OUTPUT, { recursive: true });
|
||||
function removeDirRobust(targetDir) {
|
||||
if (!fs.existsSync(targetDir)) return;
|
||||
|
||||
try {
|
||||
fs.rmSync(targetDir, {
|
||||
recursive: true,
|
||||
force: true,
|
||||
maxRetries: 10,
|
||||
retryDelay: 100,
|
||||
});
|
||||
} catch (error) {
|
||||
const code = error && typeof error === 'object' && 'code' in error ? error.code : undefined;
|
||||
if (fs.existsSync(targetDir) && (code === 'EACCES' || code === 'ENOTEMPTY' || code === 'EPERM')) {
|
||||
try {
|
||||
fs.removeSync(targetDir);
|
||||
} catch {
|
||||
// fall through to final existence check below
|
||||
}
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
if (fs.existsSync(targetDir)) {
|
||||
throw new Error(`Failed to remove directory: ${targetDir}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Clean and create output directory
|
||||
removeDirRobust(OUTPUT);
|
||||
fs.mkdirSync(OUTPUT, { recursive: true });
|
||||
|
||||
// 3. Copy openclaw package itself to OUTPUT root
|
||||
@@ -458,7 +484,13 @@ function rmSafe(target) {
|
||||
function cleanupBundle(outputDir) {
|
||||
let removedCount = 0;
|
||||
const nm = path.join(outputDir, 'node_modules');
|
||||
const ext = path.join(outputDir, 'extensions');
|
||||
// OpenClaw 3.x ships built-in extensions under dist/extensions/<ext>/, not
|
||||
// extensions/. The previous `path.join(outputDir, 'extensions')` silently
|
||||
// resolved to a non-existent directory so the entire walkExt() pass below
|
||||
// (which is what cleans .d.ts / .d.mts / source maps inside per-extension
|
||||
// node_modules) was a no-op. That left ~28k .d.mts files in the bundle and
|
||||
// contributed to the macOS codesign EMFILE blow-up.
|
||||
const ext = path.join(outputDir, 'dist', 'extensions');
|
||||
|
||||
// --- openclaw root junk ---
|
||||
for (const name of ['CHANGELOG.md', 'README.md']) {
|
||||
@@ -475,7 +507,17 @@ function cleanupBundle(outputDir) {
|
||||
const NM_REMOVE_DIRS = new Set([
|
||||
'test', 'tests', '__tests__', '.github', 'docs', 'examples', 'example',
|
||||
]);
|
||||
const NM_REMOVE_FILE_EXTS = ['.d.ts', '.d.ts.map', '.js.map', '.mjs.map', '.ts.map', '.markdown'];
|
||||
// .d.mts / .d.cts are TypeScript declaration files for ESM/CJS dual-package
|
||||
// builds. They are useless at runtime but show up in huge volumes from
|
||||
// typed packages (e.g. typebox), and inflate the per-process file count
|
||||
// that codesign opens during macOS signing → EMFILE.
|
||||
const NM_REMOVE_FILE_EXTS = [
|
||||
'.d.ts', '.d.ts.map',
|
||||
'.d.mts', '.d.mts.map',
|
||||
'.d.cts', '.d.cts.map',
|
||||
'.js.map', '.mjs.map', '.cjs.map', '.ts.map',
|
||||
'.markdown',
|
||||
];
|
||||
const NM_REMOVE_FILE_NAMES = new Set([
|
||||
'.DS_Store', 'README.md', 'CHANGELOG.md', 'LICENSE.md', 'CONTRIBUTING.md',
|
||||
'tsconfig.json', '.npmignore', '.eslintrc', '.prettierrc', '.editorconfig',
|
||||
@@ -529,7 +571,13 @@ function cleanupBundle(outputDir) {
|
||||
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_EXTS = [
|
||||
'.d.ts', '.d.ts.map',
|
||||
'.d.mts', '.d.mts.map',
|
||||
'.d.cts', '.d.cts.map',
|
||||
'.js.map', '.mjs.map', '.cjs.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',
|
||||
@@ -565,7 +613,7 @@ function cleanupBundle(outputDir) {
|
||||
'node_modules/koffi/src',
|
||||
'node_modules/koffi/vendor',
|
||||
'node_modules/koffi/doc',
|
||||
'extensions/feishu', // Removed in favor of official @larksuite/openclaw-lark plugin
|
||||
'dist/extensions/feishu', // Removed in favor of official @larksuite/openclaw-lark plugin
|
||||
];
|
||||
for (const rel of LARGE_REMOVALS) {
|
||||
if (rmSafe(path.join(outputDir, rel))) removedCount++;
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { spawn } from 'node:child_process';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const ROOT = path.resolve(__dirname, '..');
|
||||
const ELECTRON_BUILDER_BIN = process.platform === 'win32'
|
||||
? path.join(ROOT, 'node_modules', '.bin', 'electron-builder.cmd')
|
||||
: path.join(ROOT, 'node_modules', '.bin', 'electron-builder');
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
function shellQuote(value) {
|
||||
return `'${String(value).replace(/'/g, `'\\''`)}'`;
|
||||
}
|
||||
|
||||
function spawnElectronBuilder() {
|
||||
if (process.platform === 'darwin') {
|
||||
const command = [
|
||||
'ulimit -n 65536 >/dev/null 2>&1 || ulimit -n 32768 >/dev/null 2>&1 || ulimit -n 16384 >/dev/null 2>&1 || true',
|
||||
`exec ${shellQuote(ELECTRON_BUILDER_BIN)}${args.length > 0 ? ` ${args.map(shellQuote).join(' ')}` : ''}`,
|
||||
].join('; ');
|
||||
|
||||
return spawn('/bin/bash', ['-lc', command], {
|
||||
cwd: ROOT,
|
||||
stdio: 'inherit',
|
||||
env: process.env,
|
||||
});
|
||||
}
|
||||
|
||||
return spawn(ELECTRON_BUILDER_BIN, args, {
|
||||
cwd: ROOT,
|
||||
stdio: 'inherit',
|
||||
env: process.env,
|
||||
shell: process.platform === 'win32',
|
||||
});
|
||||
}
|
||||
|
||||
const child = spawnElectronBuilder();
|
||||
child.on('exit', (code, signal) => {
|
||||
if (signal) {
|
||||
process.kill(process.pid, signal);
|
||||
return;
|
||||
}
|
||||
process.exit(code ?? 1);
|
||||
});
|
||||
child.on('error', (error) => {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user