diff --git a/electron-builder.yml b/electron-builder.yml index 8547e4b7f..09b19e0d5 100644 --- a/electron-builder.yml +++ b/electron-builder.yml @@ -1,7 +1,7 @@ appId: app.clawx.desktop productName: ClawX copyright: Copyright © 2026 ClawX -compression: normal +compression: maximum artifactName: ${productName}-${version}-${os}-${arch}.${ext} directories: @@ -21,6 +21,7 @@ extraResources: - "!icons/*.md" - "!icons/*.svg" - "!bin/**" + - "!screenshot/**" # OpenClaw package (node_modules copied separately by afterPack hook # because electron-builder respects .gitignore which excludes node_modules/) - from: build/openclaw/ diff --git a/package.json b/package.json index 3a05d6fa3..b7494d20a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "clawx", - "version": "0.1.16", + "version": "0.1.17-beta.0", "pnpm": { "onlyBuiltDependencies": [ "@whiskeysockets/baileys", @@ -42,6 +42,13 @@ "postversion": "git push && git push --tags" }, "dependencies": { + "electron-store": "^11.0.2", + "electron-updater": "^6.8.3", + "ws": "^8.19.0" + }, + "devDependencies": { + "@eslint/js": "^10.0.1", + "@playwright/test": "^1.58.2", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-label": "^2.1.8", @@ -54,30 +61,6 @@ "@radix-ui/react-tabs": "^1.1.13", "@radix-ui/react-toast": "^1.2.15", "@radix-ui/react-tooltip": "^1.2.8", - "class-variance-authority": "^0.7.1", - "clawhub": "^0.5.0", - "clsx": "^2.1.1", - "electron-store": "^11.0.2", - "electron-updater": "^6.8.3", - "framer-motion": "^12.34.2", - "i18next": "^25.8.11", - "lucide-react": "^0.563.0", - "openclaw": "2026.2.23", - "react": "^19.2.4", - "react-dom": "^19.2.4", - "react-i18next": "^16.5.4", - "react-markdown": "^10.1.0", - "react-router-dom": "^7.13.0", - "remark-gfm": "^4.0.1", - "sonner": "^2.0.7", - "tailwind-merge": "^3.5.0", - "tailwindcss-animate": "^1.0.7", - "ws": "^8.19.0", - "zustand": "^5.0.11" - }, - "devDependencies": { - "@eslint/js": "^10.0.1", - "@playwright/test": "^1.58.2", "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.2", "@types/node": "^25.3.0", @@ -88,22 +71,39 @@ "@typescript-eslint/parser": "^8.56.0", "@vitejs/plugin-react": "^5.1.4", "autoprefixer": "^10.4.24", + "class-variance-authority": "^0.7.1", + "clawhub": "^0.5.0", + "clsx": "^2.1.1", "electron": "^40.6.0", "electron-builder": "^26.8.1", "eslint": "^10.0.0", "eslint-plugin-react-hooks": "^7.0.1", "eslint-plugin-react-refresh": "^0.5.0", + "framer-motion": "^12.34.2", "globals": "^17.3.0", + "i18next": "^25.8.11", "jsdom": "^28.1.0", + "lucide-react": "^0.563.0", + "openclaw": "2026.2.23", "png2icons": "^2.0.1", "postcss": "^8.5.6", + "react": "^19.2.4", + "react-dom": "^19.2.4", + "react-i18next": "^16.5.4", + "react-markdown": "^10.1.0", + "react-router-dom": "^7.13.0", + "remark-gfm": "^4.0.1", "sharp": "^0.34.5", + "sonner": "^2.0.7", + "tailwind-merge": "^3.5.0", "tailwindcss": "^3.4.19", + "tailwindcss-animate": "^1.0.7", "typescript": "^5.9.3", "vite": "^7.3.1", "vite-plugin-electron": "^0.29.0", "vite-plugin-electron-renderer": "^0.14.6", "vitest": "^4.0.18", + "zustand": "^5.0.11", "zx": "^8.8.5" }, "packageManager": "pnpm@10.29.2+sha512.bef43fa759d91fd2da4b319a5a0d13ef7a45bb985a3d7342058470f9d2051a3ba8674e629672654686ef9443ad13a82da2beb9eeb3e0221c87b8154fff9d74b8" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0aa7afb9e..3d1445c7d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,22 @@ importers: .: dependencies: + electron-store: + specifier: ^11.0.2 + version: 11.0.2 + electron-updater: + specifier: ^6.8.3 + version: 6.8.3 + ws: + specifier: ^8.19.0 + version: 8.19.0 + devDependencies: + '@eslint/js': + specifier: ^10.0.1 + version: 10.0.1(eslint@10.0.0(jiti@1.21.7)) + '@playwright/test': + specifier: ^1.58.2 + version: 1.58.2 '@radix-ui/react-dialog': specifier: ^1.1.15 version: 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -44,73 +60,6 @@ importers: '@radix-ui/react-tooltip': specifier: ^1.2.8 version: 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - class-variance-authority: - specifier: ^0.7.1 - version: 0.7.1 - clawhub: - specifier: ^0.5.0 - version: 0.5.0 - clsx: - specifier: ^2.1.1 - version: 2.1.1 - electron-store: - specifier: ^11.0.2 - version: 11.0.2 - electron-updater: - specifier: ^6.8.3 - version: 6.8.3 - framer-motion: - specifier: ^12.34.2 - version: 12.34.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - i18next: - specifier: ^25.8.11 - version: 25.8.11(typescript@5.9.3) - lucide-react: - specifier: ^0.563.0 - version: 0.563.0(react@19.2.4) - openclaw: - specifier: 2026.2.23 - version: 2026.2.23(@napi-rs/canvas@0.1.93)(@types/express@5.0.6)(encoding@0.1.13)(hono@4.11.8)(node-llama-cpp@3.15.1(typescript@5.9.3)) - react: - specifier: ^19.2.4 - version: 19.2.4 - react-dom: - specifier: ^19.2.4 - version: 19.2.4(react@19.2.4) - react-i18next: - specifier: ^16.5.4 - version: 16.5.4(i18next@25.8.11(typescript@5.9.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3) - react-markdown: - specifier: ^10.1.0 - version: 10.1.0(@types/react@19.2.14)(react@19.2.4) - react-router-dom: - specifier: ^7.13.0 - version: 7.13.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - remark-gfm: - specifier: ^4.0.1 - version: 4.0.1 - sonner: - specifier: ^2.0.7 - version: 2.0.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - tailwind-merge: - specifier: ^3.5.0 - version: 3.5.0 - tailwindcss-animate: - specifier: ^1.0.7 - version: 1.0.7(tailwindcss@3.4.19(yaml@2.8.2)) - ws: - specifier: ^8.19.0 - version: 8.19.0 - zustand: - specifier: ^5.0.11 - version: 5.0.11(@types/react@19.2.14)(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4)) - devDependencies: - '@eslint/js': - specifier: ^10.0.1 - version: 10.0.1(eslint@10.0.0(jiti@1.21.7)) - '@playwright/test': - specifier: ^1.58.2 - version: 1.58.2 '@testing-library/jest-dom': specifier: ^6.9.1 version: 6.9.1 @@ -141,6 +90,15 @@ importers: autoprefixer: specifier: ^10.4.24 version: 10.4.24(postcss@8.5.6) + class-variance-authority: + specifier: ^0.7.1 + version: 0.7.1 + clawhub: + specifier: ^0.5.0 + version: 0.5.0 + clsx: + specifier: ^2.1.1 + version: 2.1.1 electron: specifier: ^40.6.0 version: 40.6.0 @@ -156,24 +114,63 @@ importers: eslint-plugin-react-refresh: specifier: ^0.5.0 version: 0.5.0(eslint@10.0.0(jiti@1.21.7)) + framer-motion: + specifier: ^12.34.2 + version: 12.34.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4) globals: specifier: ^17.3.0 version: 17.3.0 + i18next: + specifier: ^25.8.11 + version: 25.8.11(typescript@5.9.3) jsdom: specifier: ^28.1.0 version: 28.1.0 + lucide-react: + specifier: ^0.563.0 + version: 0.563.0(react@19.2.4) + openclaw: + specifier: 2026.2.23 + version: 2026.2.23(@napi-rs/canvas@0.1.93)(@types/express@5.0.6)(encoding@0.1.13)(hono@4.11.8)(node-llama-cpp@3.15.1(typescript@5.9.3)) png2icons: specifier: ^2.0.1 version: 2.0.1 postcss: specifier: ^8.5.6 version: 8.5.6 + react: + specifier: ^19.2.4 + version: 19.2.4 + react-dom: + specifier: ^19.2.4 + version: 19.2.4(react@19.2.4) + react-i18next: + specifier: ^16.5.4 + version: 16.5.4(i18next@25.8.11(typescript@5.9.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3) + react-markdown: + specifier: ^10.1.0 + version: 10.1.0(@types/react@19.2.14)(react@19.2.4) + react-router-dom: + specifier: ^7.13.0 + version: 7.13.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + remark-gfm: + specifier: ^4.0.1 + version: 4.0.1 sharp: specifier: ^0.34.5 version: 0.34.5 + sonner: + specifier: ^2.0.7 + version: 2.0.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + tailwind-merge: + specifier: ^3.5.0 + version: 3.5.0 tailwindcss: specifier: ^3.4.19 version: 3.4.19(yaml@2.8.2) + tailwindcss-animate: + specifier: ^1.0.7 + version: 1.0.7(tailwindcss@3.4.19(yaml@2.8.2)) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -189,6 +186,9 @@ importers: vitest: specifier: ^4.0.18 version: 4.0.18(@types/node@25.3.0)(jiti@1.21.7)(jsdom@28.1.0)(yaml@2.8.2) + zustand: + specifier: ^5.0.11 + version: 5.0.11(@types/react@19.2.14)(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4)) zx: specifier: ^8.8.5 version: 8.8.5 diff --git a/scripts/after-pack.cjs b/scripts/after-pack.cjs index 675f3551c..361a21f53 100644 --- a/scripts/after-pack.cjs +++ b/scripts/after-pack.cjs @@ -10,73 +10,135 @@ * Solution: This hook runs AFTER electron-builder finishes packing. It manually * copies build/openclaw/node_modules/ into the output resources directory, * bypassing electron-builder's glob filtering entirely. - * - * Additionally, it removes unnecessary files (type definitions, source maps, docs) - * to reduce the number of files that need to be code-signed on macOS. + * + * Additionally it performs two rounds of cleanup: + * 1. General cleanup — removes dev artifacts (type defs, source maps, docs, + * test dirs) from both the openclaw root and its node_modules. + * 2. Platform-specific cleanup — strips native binaries for non-target + * platforms (koffi multi-platform prebuilds, @napi-rs/canvas, @img/sharp, + * @mariozechner/clipboard). */ const { cpSync, existsSync, readdirSync, rmSync, statSync } = require('fs'); const { join } = require('path'); -/** - * Recursively remove unnecessary files to reduce code signing overhead - */ +// ── Arch helpers ───────────────────────────────────────────────────────────── +// electron-builder Arch enum: 0=ia32, 1=x64, 2=armv7l, 3=arm64, 4=universal +const ARCH_MAP = { 0: 'ia32', 1: 'x64', 2: 'armv7l', 3: 'arm64', 4: 'universal' }; + +function resolveArch(archEnum) { + return ARCH_MAP[archEnum] || 'x64'; +} + +// ── General cleanup ────────────────────────────────────────────────────────── + function cleanupUnnecessaryFiles(dir) { let removedCount = 0; - + + 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']; + 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) { - const entries = readdirSync(currentDir, { withFileTypes: true }); - + let entries; + try { entries = readdirSync(currentDir, { withFileTypes: true }); } catch { return; } + for (const entry of entries) { const fullPath = join(currentDir, entry.name); - + if (entry.isDirectory()) { - // Remove entire test directories - if (entry.name === 'test' || entry.name === 'tests' || - entry.name === '__tests__' || entry.name === '.github' || - entry.name === 'docs' || entry.name === 'examples') { - try { - rmSync(fullPath, { recursive: true, force: true }); - removedCount++; - } catch (err) { - // Ignore errors - } + if (REMOVE_DIRS.has(entry.name)) { + try { rmSync(fullPath, { recursive: true, force: true }); removedCount++; } catch { /* */ } } else { walk(fullPath); } } else if (entry.isFile()) { const name = entry.name; - // Remove unnecessary file types - if (name.endsWith('.d.ts') || name.endsWith('.d.ts.map') || - name.endsWith('.js.map') || name.endsWith('.mjs.map') || - name.endsWith('.ts.map') || name === '.DS_Store' || - name === 'README.md' || name === 'CHANGELOG.md' || - name === 'LICENSE.md' || name === 'CONTRIBUTING.md' || - name.endsWith('.md.txt') || name.endsWith('.markdown') || - name === 'tsconfig.json' || name === '.npmignore' || - name === '.eslintrc' || name === '.prettierrc') { - try { - rmSync(fullPath, { force: true }); - removedCount++; - } catch (err) { - // Ignore errors - } + if (REMOVE_FILE_NAMES.has(name) || REMOVE_FILE_EXTS.some(e => name.endsWith(e))) { + try { rmSync(fullPath, { force: true }); removedCount++; } catch { /* */ } } } } } - + walk(dir); return removedCount; } +// ── Platform-specific: koffi ───────────────────────────────────────────────── +// koffi ships 18 platform pre-builds under koffi/build/koffi/{platform}_{arch}/. +// We only need the one matching the target. + +function cleanupKoffi(nodeModulesDir, platform, arch) { + const koffiDir = join(nodeModulesDir, 'koffi', 'build', 'koffi'); + if (!existsSync(koffiDir)) return 0; + + const keepTarget = `${platform}_${arch}`; + let removed = 0; + for (const entry of readdirSync(koffiDir)) { + if (entry !== keepTarget) { + try { rmSync(join(koffiDir, entry), { recursive: true, force: true }); removed++; } catch { /* */ } + } + } + return removed; +} + +// ── Platform-specific: scoped native packages ──────────────────────────────── +// Packages like @napi-rs/canvas-darwin-arm64, @img/sharp-linux-x64, etc. +// Only the variant matching the target platform should survive. + +const PLATFORM_NATIVE_SCOPES = { + '@napi-rs': /^canvas-(darwin|linux|win32)-(x64|arm64)/, + '@img': /^sharp(?:-libvips)?-(darwin|linux|win32)-(x64|arm64)/, + '@mariozechner': /^clipboard-(darwin|linux|win32)-(x64|arm64|universal)/, +}; + +function cleanupNativePlatformPackages(nodeModulesDir, platform, arch) { + let removed = 0; + + for (const [scope, pattern] of Object.entries(PLATFORM_NATIVE_SCOPES)) { + const scopeDir = join(nodeModulesDir, scope); + if (!existsSync(scopeDir)) continue; + + for (const entry of readdirSync(scopeDir)) { + const match = entry.match(pattern); + if (!match) continue; // not a platform-specific package, leave it + + const pkgPlatform = match[1]; + const pkgArch = match[2]; + + const isMatch = + pkgPlatform === platform && + (pkgArch === arch || pkgArch === 'universal'); + + if (!isMatch) { + try { + rmSync(join(scopeDir, entry), { recursive: true, force: true }); + removed++; + } catch { /* */ } + } + } + } + + return removed; +} + +// ── Main hook ──────────────────────────────────────────────────────────────── + exports.default = async function afterPack(context) { const appOutDir = context.appOutDir; const platform = context.electronPlatformName; // 'win32' | 'darwin' | 'linux' + const arch = resolveArch(context.arch); + + console.log(`[after-pack] Target: ${platform}/${arch}`); const src = join(__dirname, '..', 'build', 'openclaw', 'node_modules'); - // On macOS, resources live inside the .app bundle let resourcesDir; if (platform === 'darwin') { const appName = context.packager.appInfo.productFilename; @@ -85,23 +147,37 @@ exports.default = async function afterPack(context) { resourcesDir = join(appOutDir, 'resources'); } - const dest = join(resourcesDir, 'openclaw', 'node_modules'); + const openclawRoot = join(resourcesDir, 'openclaw'); + const dest = join(openclawRoot, 'node_modules'); if (!existsSync(src)) { - console.warn('[after-pack] ⚠️ build/openclaw/node_modules not found. Run "pnpm run bundle:openclaw" first.'); + console.warn('[after-pack] ⚠️ build/openclaw/node_modules not found. Run bundle-openclaw first.'); return; } + // 1. Copy node_modules (electron-builder skips it due to .gitignore) const depCount = readdirSync(src, { withFileTypes: true }) .filter(d => d.isDirectory() && d.name !== '.bin') .length; console.log(`[after-pack] Copying ${depCount} openclaw dependencies to ${dest} ...`); cpSync(src, dest, { recursive: true }); - console.log('[after-pack] ✅ openclaw node_modules copied successfully.'); - - // Clean up unnecessary files to reduce code signing overhead (especially on macOS) - console.log('[after-pack] 🧹 Cleaning up unnecessary files (type definitions, source maps, docs)...'); - const removedCount = cleanupUnnecessaryFiles(dest); - console.log(`[after-pack] ✅ Removed ${removedCount} unnecessary files/directories.`); + console.log('[after-pack] ✅ openclaw node_modules copied.'); + + // 2. General cleanup on the full openclaw directory (not just node_modules) + console.log('[after-pack] 🧹 Cleaning up unnecessary files ...'); + const removedRoot = cleanupUnnecessaryFiles(openclawRoot); + console.log(`[after-pack] ✅ Removed ${removedRoot} unnecessary files/directories.`); + + // 3. Platform-specific: strip koffi non-target platform binaries + const koffiRemoved = cleanupKoffi(dest, platform, arch); + if (koffiRemoved > 0) { + console.log(`[after-pack] ✅ koffi: removed ${koffiRemoved} non-target platform binaries (kept ${platform}_${arch}).`); + } + + // 4. Platform-specific: strip wrong-platform native packages + const nativeRemoved = cleanupNativePlatformPackages(dest, platform, arch); + if (nativeRemoved > 0) { + console.log(`[after-pack] ✅ Removed ${nativeRemoved} non-target native platform packages.`); + } }; diff --git a/scripts/bundle-openclaw.mjs b/scripts/bundle-openclaw.mjs index d0649716f..15d8bbefe 100644 --- a/scripts/bundle-openclaw.mjs +++ b/scripts/bundle-openclaw.mjs @@ -127,6 +127,14 @@ if (!openclawVirtualNM) { echo` Virtual store root: ${openclawVirtualNM}`; queue.push({ nodeModulesDir: openclawVirtualNM, skipPkg: 'openclaw' }); +const SKIP_PACKAGES = new Set([ + 'typescript', + 'playwright-core', + '@playwright/test', +]); +const SKIP_SCOPES = ['@cloudflare/', '@types/']; +let skippedDevCount = 0; + while (queue.length > 0) { const { nodeModulesDir, skipPkg } = queue.shift(); const packages = listPackages(nodeModulesDir); @@ -135,6 +143,11 @@ while (queue.length > 0) { // Skip the package that owns this virtual store entry (it's the package itself, not a dep) if (name === skipPkg) continue; + if (SKIP_PACKAGES.has(name) || SKIP_SCOPES.some(s => name.startsWith(s))) { + skippedDevCount++; + continue; + } + let realPath; try { realPath = fs.realpathSync(fullPath); @@ -156,6 +169,7 @@ while (queue.length > 0) { } echo` Found ${collected.size} total packages (direct + transitive)`; +echo` Skipped ${skippedDevCount} dev-only package references`; // 5. Copy all collected packages into OUTPUT/node_modules/ (flat structure) // @@ -190,13 +204,160 @@ for (const [realPath, pkgName] of collected) { } } -// 6. Verify the bundle +// 6. Clean up the bundle to reduce package size +// +// This removes platform-agnostic waste: dev artifacts, docs, source maps, +// type definitions, test directories, and known large unused subdirectories. +// Platform-specific cleanup (e.g. koffi binaries) is handled in after-pack.cjs +// which has access to the target platform/arch context. + +function getDirSize(dir) { + let total = 0; + try { + for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { + const p = path.join(dir, entry.name); + if (entry.isDirectory()) total += getDirSize(p); + else if (entry.isFile()) total += fs.statSync(p).size; + } + } catch { /* ignore */ } + return total; +} + +function formatSize(bytes) { + if (bytes >= 1024 * 1024 * 1024) return `${(bytes / 1024 / 1024 / 1024).toFixed(1)}G`; + if (bytes >= 1024 * 1024) return `${(bytes / 1024 / 1024).toFixed(1)}M`; + if (bytes >= 1024) return `${(bytes / 1024).toFixed(1)}K`; + return `${bytes}B`; +} + +function rmSafe(target) { + try { + const stat = fs.lstatSync(target); + if (stat.isDirectory()) fs.rmSync(target, { recursive: true, force: true }); + else fs.rmSync(target, { force: true }); + return true; + } catch { return false; } +} + +function cleanupBundle(outputDir) { + let removedCount = 0; + const nm = path.join(outputDir, 'node_modules'); + const ext = path.join(outputDir, 'extensions'); + + // --- openclaw root junk --- + for (const name of ['CHANGELOG.md', 'README.md']) { + if (rmSafe(path.join(outputDir, name))) removedCount++; + } + + // docs/ is kept — contains prompt templates and other runtime-used prompts + + // --- extensions: clean junk from source, aggressively clean nested node_modules --- + // Extension source (.ts files) are runtime entry points — must be preserved. + // Only nested node_modules/ inside extensions get the aggressive cleanup. + if (fs.existsSync(ext)) { + const JUNK_EXTS = new Set(['.prose', '.ignored_openclaw', '.keep']); + 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']; + const NM_REMOVE_FILE_NAMES = new Set([ + '.DS_Store', 'README.md', 'CHANGELOG.md', 'LICENSE.md', 'CONTRIBUTING.md', + 'tsconfig.json', '.npmignore', '.eslintrc', '.prettierrc', '.editorconfig', + ]); + + function walkExt(dir, insideNodeModules) { + let entries; + try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; } + for (const entry of entries) { + const full = path.join(dir, entry.name); + if (entry.isDirectory()) { + if (insideNodeModules && NM_REMOVE_DIRS.has(entry.name)) { + if (rmSafe(full)) removedCount++; + } else { + walkExt(full, insideNodeModules || entry.name === 'node_modules'); + } + } else if (entry.isFile()) { + if (insideNodeModules) { + const name = entry.name; + if (NM_REMOVE_FILE_NAMES.has(name) || NM_REMOVE_FILE_EXTS.some(e => name.endsWith(e))) { + if (rmSafe(full)) removedCount++; + } + } else { + if (JUNK_EXTS.has(path.extname(entry.name)) || entry.name.endsWith('.md')) { + if (rmSafe(full)) removedCount++; + } + } + } + } + } + walkExt(ext, false); + } + + // --- node_modules: remove unnecessary file types and directories --- + if (fs.existsSync(nm)) { + 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 walkClean(dir) { + let entries; + try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; } + for (const entry of entries) { + const full = path.join(dir, entry.name); + if (entry.isDirectory()) { + if (REMOVE_DIRS.has(entry.name)) { + if (rmSafe(full)) removedCount++; + } else { + walkClean(full); + } + } else if (entry.isFile()) { + const name = entry.name; + if (REMOVE_FILE_NAMES.has(name) || REMOVE_FILE_EXTS.some(e => name.endsWith(e))) { + if (rmSafe(full)) removedCount++; + } + } + } + } + walkClean(nm); + } + + // --- known large unused subdirectories --- + const LARGE_REMOVALS = [ + 'node_modules/pdfjs-dist/legacy', + 'node_modules/pdfjs-dist/types', + 'node_modules/node-llama-cpp/llama', + 'node_modules/koffi/src', + 'node_modules/koffi/vendor', + 'node_modules/koffi/doc', + ]; + for (const rel of LARGE_REMOVALS) { + if (rmSafe(path.join(outputDir, rel))) removedCount++; + } + + return removedCount; +} + +echo``; +echo`🧹 Cleaning up bundle (removing dev artifacts, docs, source maps, type defs)...`; +const sizeBefore = getDirSize(OUTPUT); +const cleanedCount = cleanupBundle(OUTPUT); +const sizeAfter = getDirSize(OUTPUT); +echo` Removed ${cleanedCount} files/directories`; +echo` Size: ${formatSize(sizeBefore)} → ${formatSize(sizeAfter)} (saved ${formatSize(sizeBefore - sizeAfter)})`; + +// 7. Verify the bundle const entryExists = fs.existsSync(path.join(OUTPUT, 'openclaw.mjs')); const distExists = fs.existsSync(path.join(OUTPUT, 'dist', 'entry.js')); echo``; echo`✅ Bundle complete: ${OUTPUT}`; echo` Unique packages copied: ${copiedCount}`; +echo` Dev-only packages skipped: ${skippedDevCount}`; echo` Duplicate versions skipped: ${skippedDupes}`; echo` Total discovered: ${collected.size}`; echo` openclaw.mjs: ${entryExists ? '✓' : '✗'}`;