Fix dingding plugin (#221)
This commit is contained in:
committed by
GitHub
Unverified
parent
98703a0ab8
commit
dbf88a79be
@@ -26,9 +26,9 @@ extraResources:
|
|||||||
# because electron-builder respects .gitignore which excludes node_modules/)
|
# because electron-builder respects .gitignore which excludes node_modules/)
|
||||||
- from: build/openclaw/
|
- from: build/openclaw/
|
||||||
to: openclaw/
|
to: openclaw/
|
||||||
# Bundled OpenClaw plugin mirrors (dingtalk, etc.)
|
# NOTE: OpenClaw plugin mirrors (dingtalk, etc.) are bundled by the
|
||||||
- from: build/openclaw-plugins/
|
# afterPack hook (after-pack.cjs) directly from node_modules, so they
|
||||||
to: openclaw-plugins/
|
# don't need an extraResources entry here.
|
||||||
|
|
||||||
afterPack: ./scripts/after-pack.cjs
|
afterPack: ./scripts/after-pack.cjs
|
||||||
|
|
||||||
|
|||||||
@@ -627,9 +627,10 @@ function registerOpenClawHandlers(gatewayManager: GatewayManager): void {
|
|||||||
|
|
||||||
const sourceDir = candidateSources.find((dir) => existsSync(join(dir, 'openclaw.plugin.json')));
|
const sourceDir = candidateSources.find((dir) => existsSync(join(dir, 'openclaw.plugin.json')));
|
||||||
if (!sourceDir) {
|
if (!sourceDir) {
|
||||||
|
logger.warn('Bundled DingTalk plugin mirror not found in candidate paths', { candidateSources });
|
||||||
return {
|
return {
|
||||||
installed: false,
|
installed: false,
|
||||||
warning: 'Bundled DingTalk plugin mirror not found. Run: pnpm run bundle:openclaw-plugins',
|
warning: `Bundled DingTalk plugin mirror not found. Checked: ${candidateSources.join(' | ')}`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -718,7 +719,8 @@ function registerOpenClawHandlers(gatewayManager: GatewayManager): void {
|
|||||||
error: installResult.warning || 'DingTalk plugin install failed',
|
error: installResult.warning || 'DingTalk plugin install failed',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
saveChannelConfig(channelType, config);
|
await saveChannelConfig(channelType, config);
|
||||||
|
gatewayManager.debouncedRestart();
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
pluginInstalled: installResult.installed,
|
pluginInstalled: installResult.installed,
|
||||||
|
|||||||
@@ -32,7 +32,7 @@
|
|||||||
"uv:download:linux": "zx scripts/download-bundled-uv.mjs --platform=linux",
|
"uv:download:linux": "zx scripts/download-bundled-uv.mjs --platform=linux",
|
||||||
"uv:download:all": "zx scripts/download-bundled-uv.mjs --all",
|
"uv:download:all": "zx scripts/download-bundled-uv.mjs --all",
|
||||||
"icons": "zx scripts/generate-icons.mjs",
|
"icons": "zx scripts/generate-icons.mjs",
|
||||||
"package": "electron-builder",
|
"package": "vite build && zx scripts/bundle-openclaw.mjs && zx scripts/bundle-openclaw-plugins.mjs && electron-builder",
|
||||||
"package:mac": "vite build && zx scripts/bundle-openclaw.mjs && zx scripts/bundle-openclaw-plugins.mjs && electron-builder --mac",
|
"package:mac": "vite build && zx scripts/bundle-openclaw.mjs && zx scripts/bundle-openclaw-plugins.mjs && electron-builder --mac",
|
||||||
"package:win": "vite build && zx scripts/bundle-openclaw.mjs && zx scripts/bundle-openclaw-plugins.mjs && electron-builder --win",
|
"package:win": "vite build && zx scripts/bundle-openclaw.mjs && zx scripts/bundle-openclaw-plugins.mjs && electron-builder --win",
|
||||||
"package:linux": "vite build && zx scripts/bundle-openclaw.mjs && zx scripts/bundle-openclaw-plugins.mjs && electron-builder --linux",
|
"package:linux": "vite build && zx scripts/bundle-openclaw.mjs && zx scripts/bundle-openclaw-plugins.mjs && electron-builder --linux",
|
||||||
|
|||||||
@@ -19,8 +19,18 @@
|
|||||||
* @mariozechner/clipboard).
|
* @mariozechner/clipboard).
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const { cpSync, existsSync, readdirSync, rmSync, statSync } = require('fs');
|
const { cpSync, existsSync, readdirSync, rmSync, statSync, mkdirSync, realpathSync } = require('fs');
|
||||||
const { join } = require('path');
|
const { join, dirname, basename } = require('path');
|
||||||
|
|
||||||
|
// On Windows, paths in pnpm's virtual store can exceed the default MAX_PATH
|
||||||
|
// limit (260 chars). Node.js 18.17+ respects the system LongPathsEnabled
|
||||||
|
// registry key, but as a safety net we normalize paths to use the \\?\ prefix
|
||||||
|
// on Windows, which bypasses the limit unconditionally.
|
||||||
|
function normWin(p) {
|
||||||
|
if (process.platform !== 'win32') return p;
|
||||||
|
if (p.startsWith('\\\\?\\')) return p;
|
||||||
|
return '\\\\?\\' + p.replace(/\//g, '\\');
|
||||||
|
}
|
||||||
|
|
||||||
// ── Arch helpers ─────────────────────────────────────────────────────────────
|
// ── Arch helpers ─────────────────────────────────────────────────────────────
|
||||||
// electron-builder Arch enum: 0=ia32, 1=x64, 2=armv7l, 3=arm64, 4=universal
|
// electron-builder Arch enum: 0=ia32, 1=x64, 2=armv7l, 3=arm64, 4=universal
|
||||||
@@ -128,6 +138,119 @@ function cleanupNativePlatformPackages(nodeModulesDir, platform, arch) {
|
|||||||
return removed;
|
return removed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Plugin bundler ───────────────────────────────────────────────────────────
|
||||||
|
// Bundles a single OpenClaw plugin (and its transitive deps) from node_modules
|
||||||
|
// directly into the packaged resources directory. Mirrors the logic in
|
||||||
|
// bundle-openclaw-plugins.mjs so the packaged app is self-contained even when
|
||||||
|
// build/openclaw-plugins/ was not pre-generated.
|
||||||
|
|
||||||
|
function getVirtualStoreNodeModules(realPkgPath) {
|
||||||
|
let dir = realPkgPath;
|
||||||
|
while (dir !== dirname(dir)) {
|
||||||
|
if (basename(dir) === 'node_modules') return dir;
|
||||||
|
dir = dirname(dir);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function listPkgs(nodeModulesDir) {
|
||||||
|
const result = [];
|
||||||
|
const nDir = normWin(nodeModulesDir);
|
||||||
|
if (!existsSync(nDir)) return result;
|
||||||
|
for (const entry of readdirSync(nDir)) {
|
||||||
|
if (entry === '.bin') continue;
|
||||||
|
// Use original (non-normWin) join for the logical path stored in result.fullPath,
|
||||||
|
// so callers can still call getVirtualStoreNodeModules() on it correctly.
|
||||||
|
const fullPath = join(nodeModulesDir, entry);
|
||||||
|
if (entry.startsWith('@')) {
|
||||||
|
let subs;
|
||||||
|
try { subs = readdirSync(normWin(fullPath)); } catch { continue; }
|
||||||
|
for (const sub of subs) {
|
||||||
|
result.push({ name: `${entry}/${sub}`, fullPath: join(fullPath, sub) });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result.push({ name: entry, fullPath });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function bundlePlugin(nodeModulesRoot, npmName, destDir) {
|
||||||
|
const pkgPath = join(nodeModulesRoot, ...npmName.split('/'));
|
||||||
|
if (!existsSync(pkgPath)) {
|
||||||
|
console.warn(`[after-pack] ⚠️ Plugin package not found: ${pkgPath}. Run pnpm install.`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let realPluginPath;
|
||||||
|
try { realPluginPath = realpathSync(normWin(pkgPath)); } catch { realPluginPath = pkgPath; }
|
||||||
|
|
||||||
|
// Copy plugin package itself
|
||||||
|
if (existsSync(normWin(destDir))) rmSync(normWin(destDir), { recursive: true, force: true });
|
||||||
|
mkdirSync(normWin(destDir), { recursive: true });
|
||||||
|
cpSync(normWin(realPluginPath), normWin(destDir), { recursive: true, dereference: true });
|
||||||
|
|
||||||
|
// Collect transitive deps via pnpm virtual store BFS
|
||||||
|
const collected = new Map();
|
||||||
|
const queue = [];
|
||||||
|
|
||||||
|
const rootVirtualNM = getVirtualStoreNodeModules(realPluginPath);
|
||||||
|
if (!rootVirtualNM) {
|
||||||
|
console.warn(`[after-pack] ⚠️ Could not find virtual store for ${npmName}, skipping deps.`);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
queue.push({ nodeModulesDir: rootVirtualNM, skipPkg: npmName });
|
||||||
|
|
||||||
|
// Read peerDependencies from the plugin's package.json so we don't bundle
|
||||||
|
// packages that are provided by the host environment (e.g. openclaw itself).
|
||||||
|
const SKIP_PACKAGES = new Set(['typescript', '@playwright/test']);
|
||||||
|
const SKIP_SCOPES = ['@types/'];
|
||||||
|
try {
|
||||||
|
const pluginPkg = JSON.parse(
|
||||||
|
require('fs').readFileSync(join(destDir, 'package.json'), 'utf8')
|
||||||
|
);
|
||||||
|
for (const peer of Object.keys(pluginPkg.peerDependencies || {})) {
|
||||||
|
SKIP_PACKAGES.add(peer);
|
||||||
|
}
|
||||||
|
} catch { /* ignore */ }
|
||||||
|
|
||||||
|
while (queue.length > 0) {
|
||||||
|
const { nodeModulesDir, skipPkg } = queue.shift();
|
||||||
|
for (const { name, fullPath } of listPkgs(nodeModulesDir)) {
|
||||||
|
if (name === skipPkg) continue;
|
||||||
|
if (SKIP_PACKAGES.has(name) || SKIP_SCOPES.some(s => name.startsWith(s))) continue;
|
||||||
|
let rp;
|
||||||
|
try { rp = realpathSync(normWin(fullPath)); } catch { continue; }
|
||||||
|
if (collected.has(rp)) continue;
|
||||||
|
collected.set(rp, name);
|
||||||
|
const depVirtualNM = getVirtualStoreNodeModules(rp);
|
||||||
|
if (depVirtualNM && depVirtualNM !== nodeModulesDir) {
|
||||||
|
queue.push({ nodeModulesDir: depVirtualNM, skipPkg: name });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy flattened deps into destDir/node_modules
|
||||||
|
const destNM = join(destDir, 'node_modules');
|
||||||
|
mkdirSync(destNM, { recursive: true });
|
||||||
|
const copiedNames = new Set();
|
||||||
|
let count = 0;
|
||||||
|
for (const [rp, pkgName] of collected) {
|
||||||
|
if (copiedNames.has(pkgName)) continue;
|
||||||
|
copiedNames.add(pkgName);
|
||||||
|
const d = join(destNM, pkgName);
|
||||||
|
try {
|
||||||
|
mkdirSync(normWin(dirname(d)), { recursive: true });
|
||||||
|
cpSync(normWin(rp), normWin(d), { recursive: true, dereference: true });
|
||||||
|
count++;
|
||||||
|
} catch (e) {
|
||||||
|
console.warn(`[after-pack] Skipped dep ${pkgName}: ${e.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log(`[after-pack] ✅ Plugin ${npmName}: copied ${count} deps to ${destDir}`);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// ── Main hook ────────────────────────────────────────────────────────────────
|
// ── Main hook ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
exports.default = async function afterPack(context) {
|
exports.default = async function afterPack(context) {
|
||||||
@@ -149,7 +272,7 @@ exports.default = async function afterPack(context) {
|
|||||||
|
|
||||||
const openclawRoot = join(resourcesDir, 'openclaw');
|
const openclawRoot = join(resourcesDir, 'openclaw');
|
||||||
const dest = join(openclawRoot, 'node_modules');
|
const dest = join(openclawRoot, 'node_modules');
|
||||||
const pluginsSrcRoot = join(__dirname, '..', 'build', 'openclaw-plugins');
|
const nodeModulesRoot = join(__dirname, '..', 'node_modules');
|
||||||
const pluginsDestRoot = join(resourcesDir, 'openclaw-plugins');
|
const pluginsDestRoot = join(resourcesDir, 'openclaw-plugins');
|
||||||
|
|
||||||
if (!existsSync(src)) {
|
if (!existsSync(src)) {
|
||||||
@@ -166,27 +289,28 @@ exports.default = async function afterPack(context) {
|
|||||||
cpSync(src, dest, { recursive: true });
|
cpSync(src, dest, { recursive: true });
|
||||||
console.log('[after-pack] ✅ openclaw node_modules copied.');
|
console.log('[after-pack] ✅ openclaw node_modules copied.');
|
||||||
|
|
||||||
// 1.1 Copy plugin node_modules (also skipped due to .gitignore)
|
// 1.1 Bundle OpenClaw plugins directly from node_modules into packaged resources.
|
||||||
if (existsSync(pluginsSrcRoot) && existsSync(pluginsDestRoot)) {
|
// This is intentionally done in afterPack (not extraResources) because:
|
||||||
const pluginDirs = readdirSync(pluginsSrcRoot, { withFileTypes: true })
|
// - electron-builder silently skips extraResources entries whose source
|
||||||
.filter((d) => d.isDirectory())
|
// directory doesn't exist (build/openclaw-plugins/ may not be pre-generated)
|
||||||
.map((d) => d.name);
|
// - node_modules/ is excluded by .gitignore so the deps copy must be manual
|
||||||
|
const BUNDLED_PLUGINS = [
|
||||||
|
{ npmName: '@soimy/dingtalk', pluginId: 'dingtalk' },
|
||||||
|
];
|
||||||
|
|
||||||
for (const pluginId of pluginDirs) {
|
mkdirSync(pluginsDestRoot, { recursive: true });
|
||||||
const pluginSrcNM = join(pluginsSrcRoot, pluginId, 'node_modules');
|
for (const { npmName, pluginId } of BUNDLED_PLUGINS) {
|
||||||
const pluginDestRoot = join(pluginsDestRoot, pluginId);
|
const pluginDestDir = join(pluginsDestRoot, pluginId);
|
||||||
const pluginDestNM = join(pluginDestRoot, 'node_modules');
|
console.log(`[after-pack] Bundling plugin ${npmName} -> ${pluginDestDir}`);
|
||||||
if (!existsSync(pluginSrcNM) || !existsSync(pluginDestRoot)) continue;
|
const ok = bundlePlugin(nodeModulesRoot, npmName, pluginDestDir);
|
||||||
|
if (ok) {
|
||||||
console.log(`[after-pack] Copying plugin deps for ${pluginId} -> ${pluginDestNM}`);
|
const pluginNM = join(pluginDestDir, 'node_modules');
|
||||||
cpSync(pluginSrcNM, pluginDestNM, { recursive: true });
|
cleanupUnnecessaryFiles(pluginDestDir);
|
||||||
|
if (existsSync(pluginNM)) {
|
||||||
// Apply the same cleanup strategy for plugin bundles.
|
cleanupKoffi(pluginNM, platform, arch);
|
||||||
cleanupUnnecessaryFiles(pluginDestRoot);
|
cleanupNativePlatformPackages(pluginNM, platform, arch);
|
||||||
cleanupKoffi(pluginDestNM, platform, arch);
|
}
|
||||||
cleanupNativePlatformPackages(pluginDestNM, platform, arch);
|
|
||||||
}
|
}
|
||||||
console.log('[after-pack] ✅ openclaw plugin node_modules copied.');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. General cleanup on the full openclaw directory (not just node_modules)
|
// 2. General cleanup on the full openclaw directory (not just node_modules)
|
||||||
|
|||||||
@@ -20,6 +20,17 @@ const ROOT = path.resolve(__dirname, '..');
|
|||||||
const OUTPUT_ROOT = path.join(ROOT, 'build', 'openclaw-plugins');
|
const OUTPUT_ROOT = path.join(ROOT, 'build', 'openclaw-plugins');
|
||||||
const NODE_MODULES = path.join(ROOT, 'node_modules');
|
const NODE_MODULES = path.join(ROOT, 'node_modules');
|
||||||
|
|
||||||
|
// On Windows, pnpm virtual store paths can exceed MAX_PATH (260 chars).
|
||||||
|
// Adding \\?\ prefix bypasses the limit for Win32 fs calls.
|
||||||
|
// Node.js 18.17+ also handles this transparently when LongPathsEnabled=1,
|
||||||
|
// but this is an extra safety net for build machines where the registry key
|
||||||
|
// may not be set yet.
|
||||||
|
function normWin(p) {
|
||||||
|
if (process.platform !== 'win32') return p;
|
||||||
|
if (p.startsWith('\\\\?\\')) return p;
|
||||||
|
return '\\\\?\\' + p.replace(/\//g, '\\');
|
||||||
|
}
|
||||||
|
|
||||||
const PLUGINS = [
|
const PLUGINS = [
|
||||||
{ npmName: '@soimy/dingtalk', pluginId: 'dingtalk' },
|
{ npmName: '@soimy/dingtalk', pluginId: 'dingtalk' },
|
||||||
];
|
];
|
||||||
@@ -35,18 +46,19 @@ function getVirtualStoreNodeModules(realPkgPath) {
|
|||||||
|
|
||||||
function listPackages(nodeModulesDir) {
|
function listPackages(nodeModulesDir) {
|
||||||
const result = [];
|
const result = [];
|
||||||
if (!fs.existsSync(nodeModulesDir)) return result;
|
const nDir = normWin(nodeModulesDir);
|
||||||
|
if (!fs.existsSync(nDir)) return result;
|
||||||
|
|
||||||
for (const entry of fs.readdirSync(nodeModulesDir)) {
|
for (const entry of fs.readdirSync(nDir)) {
|
||||||
if (entry === '.bin') continue;
|
if (entry === '.bin') continue;
|
||||||
|
// Use original (non-normWin) path so callers can call
|
||||||
|
// getVirtualStoreNodeModules() on fullPath correctly.
|
||||||
const entryPath = path.join(nodeModulesDir, entry);
|
const entryPath = path.join(nodeModulesDir, entry);
|
||||||
const stat = fs.lstatSync(entryPath);
|
|
||||||
|
|
||||||
if (entry.startsWith('@')) {
|
if (entry.startsWith('@')) {
|
||||||
if (!(stat.isDirectory() || stat.isSymbolicLink())) continue;
|
|
||||||
let scopeEntries = [];
|
let scopeEntries = [];
|
||||||
try {
|
try {
|
||||||
scopeEntries = fs.readdirSync(entryPath);
|
scopeEntries = fs.readdirSync(normWin(entryPath));
|
||||||
} catch {
|
} catch {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -69,7 +81,7 @@ function bundleOnePlugin({ npmName, pluginId }) {
|
|||||||
throw new Error(`Missing dependency "${npmName}". Run pnpm install first.`);
|
throw new Error(`Missing dependency "${npmName}". Run pnpm install first.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const realPluginPath = fs.realpathSync(pkgPath);
|
const realPluginPath = fs.realpathSync(normWin(pkgPath));
|
||||||
const outputDir = path.join(OUTPUT_ROOT, pluginId);
|
const outputDir = path.join(OUTPUT_ROOT, pluginId);
|
||||||
|
|
||||||
echo`📦 Bundling plugin ${npmName} -> ${outputDir}`;
|
echo`📦 Bundling plugin ${npmName} -> ${outputDir}`;
|
||||||
@@ -91,8 +103,15 @@ function bundleOnePlugin({ npmName, pluginId }) {
|
|||||||
}
|
}
|
||||||
queue.push({ nodeModulesDir: rootVirtualNM, skipPkg: npmName });
|
queue.push({ nodeModulesDir: rootVirtualNM, skipPkg: npmName });
|
||||||
|
|
||||||
|
// Skip peerDependencies — they're provided by the host openclaw gateway.
|
||||||
const SKIP_PACKAGES = new Set(['typescript', '@playwright/test']);
|
const SKIP_PACKAGES = new Set(['typescript', '@playwright/test']);
|
||||||
const SKIP_SCOPES = ['@types/'];
|
const SKIP_SCOPES = ['@types/'];
|
||||||
|
try {
|
||||||
|
const pluginPkg = JSON.parse(fs.readFileSync(path.join(outputDir, 'package.json'), 'utf8'));
|
||||||
|
for (const peer of Object.keys(pluginPkg.peerDependencies || {})) {
|
||||||
|
SKIP_PACKAGES.add(peer);
|
||||||
|
}
|
||||||
|
} catch { /* ignore */ }
|
||||||
|
|
||||||
while (queue.length > 0) {
|
while (queue.length > 0) {
|
||||||
const { nodeModulesDir, skipPkg } = queue.shift();
|
const { nodeModulesDir, skipPkg } = queue.shift();
|
||||||
@@ -102,7 +121,7 @@ function bundleOnePlugin({ npmName, pluginId }) {
|
|||||||
|
|
||||||
let realPath;
|
let realPath;
|
||||||
try {
|
try {
|
||||||
realPath = fs.realpathSync(fullPath);
|
realPath = fs.realpathSync(normWin(fullPath));
|
||||||
} catch {
|
} catch {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -133,8 +152,8 @@ function bundleOnePlugin({ npmName, pluginId }) {
|
|||||||
|
|
||||||
const dest = path.join(outputNodeModules, pkgName);
|
const dest = path.join(outputNodeModules, pkgName);
|
||||||
try {
|
try {
|
||||||
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
fs.mkdirSync(normWin(path.dirname(dest)), { recursive: true });
|
||||||
fs.cpSync(realPath, dest, { recursive: true, dereference: true });
|
fs.cpSync(normWin(realPath), normWin(dest), { recursive: true, dereference: true });
|
||||||
copiedCount++;
|
copiedCount++;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
echo` ⚠️ Skipped ${pkgName}: ${err.message}`;
|
echo` ⚠️ Skipped ${pkgName}: ${err.message}`;
|
||||||
|
|||||||
@@ -22,6 +22,13 @@ const ROOT = path.resolve(__dirname, '..');
|
|||||||
const OUTPUT = path.join(ROOT, 'build', 'openclaw');
|
const OUTPUT = path.join(ROOT, 'build', 'openclaw');
|
||||||
const NODE_MODULES = path.join(ROOT, 'node_modules');
|
const NODE_MODULES = path.join(ROOT, 'node_modules');
|
||||||
|
|
||||||
|
// On Windows, pnpm virtual store paths can exceed MAX_PATH (260 chars).
|
||||||
|
function normWin(p) {
|
||||||
|
if (process.platform !== 'win32') return p;
|
||||||
|
if (p.startsWith('\\\\?\\')) return p;
|
||||||
|
return '\\\\?\\' + p.replace(/\//g, '\\');
|
||||||
|
}
|
||||||
|
|
||||||
echo`📦 Bundling openclaw for electron-builder...`;
|
echo`📦 Bundling openclaw for electron-builder...`;
|
||||||
|
|
||||||
// 1. Resolve the real path of node_modules/openclaw (follows pnpm symlink)
|
// 1. Resolve the real path of node_modules/openclaw (follows pnpm symlink)
|
||||||
@@ -31,7 +38,7 @@ if (!fs.existsSync(openclawLink)) {
|
|||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
const openclawReal = fs.realpathSync(openclawLink);
|
const openclawReal = fs.realpathSync(normWin(openclawLink));
|
||||||
echo` openclaw resolved: ${openclawReal}`;
|
echo` openclaw resolved: ${openclawReal}`;
|
||||||
|
|
||||||
// 2. Clean and create output directory
|
// 2. Clean and create output directory
|
||||||
@@ -85,30 +92,26 @@ function getVirtualStoreNodeModules(realPkgPath) {
|
|||||||
*/
|
*/
|
||||||
function listPackages(nodeModulesDir) {
|
function listPackages(nodeModulesDir) {
|
||||||
const result = [];
|
const result = [];
|
||||||
if (!fs.existsSync(nodeModulesDir)) return result;
|
const nDir = normWin(nodeModulesDir);
|
||||||
|
if (!fs.existsSync(nDir)) return result;
|
||||||
|
|
||||||
for (const entry of fs.readdirSync(nodeModulesDir)) {
|
for (const entry of fs.readdirSync(nDir)) {
|
||||||
if (entry === '.bin') continue;
|
if (entry === '.bin') continue;
|
||||||
|
// Use original (non-normWin) path so callers can call
|
||||||
|
// getVirtualStoreNodeModules() on fullPath correctly.
|
||||||
const entryPath = path.join(nodeModulesDir, entry);
|
const entryPath = path.join(nodeModulesDir, entry);
|
||||||
const stat = fs.lstatSync(entryPath);
|
|
||||||
|
|
||||||
if (entry.startsWith('@')) {
|
if (entry.startsWith('@')) {
|
||||||
// Scoped package: read sub-entries
|
try {
|
||||||
if (stat.isDirectory() || stat.isSymbolicLink()) {
|
const scopeEntries = fs.readdirSync(normWin(entryPath));
|
||||||
const resolvedScope = stat.isSymbolicLink() ? fs.realpathSync(entryPath) : entryPath;
|
for (const sub of scopeEntries) {
|
||||||
// Check if this is actually a scoped directory or a package
|
result.push({
|
||||||
try {
|
name: `${entry}/${sub}`,
|
||||||
const scopeEntries = fs.readdirSync(entryPath);
|
fullPath: path.join(entryPath, sub),
|
||||||
for (const sub of scopeEntries) {
|
});
|
||||||
result.push({
|
|
||||||
name: `${entry}/${sub}`,
|
|
||||||
fullPath: path.join(entryPath, sub),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// Not a directory, skip
|
|
||||||
}
|
}
|
||||||
|
} catch {
|
||||||
|
// Not a directory, skip
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
result.push({ name: entry, fullPath: entryPath });
|
result.push({ name: entry, fullPath: entryPath });
|
||||||
@@ -149,7 +152,7 @@ while (queue.length > 0) {
|
|||||||
|
|
||||||
let realPath;
|
let realPath;
|
||||||
try {
|
try {
|
||||||
realPath = fs.realpathSync(fullPath);
|
realPath = fs.realpathSync(normWin(fullPath));
|
||||||
} catch {
|
} catch {
|
||||||
continue; // broken symlink, skip
|
continue; // broken symlink, skip
|
||||||
}
|
}
|
||||||
@@ -194,9 +197,8 @@ for (const [realPath, pkgName] of collected) {
|
|||||||
const dest = path.join(outputNodeModules, pkgName);
|
const dest = path.join(outputNodeModules, pkgName);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Ensure parent directory exists (for scoped packages like @clack/core)
|
fs.mkdirSync(normWin(path.dirname(dest)), { recursive: true });
|
||||||
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
fs.cpSync(normWin(realPath), normWin(dest), { recursive: true, dereference: true });
|
||||||
fs.cpSync(realPath, dest, { recursive: true, dereference: true });
|
|
||||||
copiedCount++;
|
copiedCount++;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
echo` ⚠️ Skipped ${pkgName}: ${err.message}`;
|
echo` ⚠️ Skipped ${pkgName}: ${err.message}`;
|
||||||
|
|||||||
@@ -1,7 +1,14 @@
|
|||||||
; ClawX Custom NSIS Uninstaller Script
|
; ClawX Custom NSIS Installer/Uninstaller Script
|
||||||
; Provides a "Complete Removal" option during uninstallation
|
|
||||||
; to delete .openclaw config and AppData resources.
|
!macro customInstall
|
||||||
; Handles both per-user and per-machine (all users) installations.
|
; Enable Windows long path support for all-user (admin) installs.
|
||||||
|
; pnpm virtual store and node_modules paths can exceed the default 260-char
|
||||||
|
; MAX_PATH limit on Windows. This registry key enables the modern NTFS
|
||||||
|
; long-path behavior on Windows 10 1607+ / Windows 11.
|
||||||
|
${If} $MultiUser.InstallMode == "AllUsers"
|
||||||
|
WriteRegDWORD HKLM "SYSTEM\CurrentControlSet\Control\FileSystem" "LongPathsEnabled" 1
|
||||||
|
${EndIf}
|
||||||
|
!macroend
|
||||||
|
|
||||||
!macro customUnInstall
|
!macro customUnInstall
|
||||||
; Ask user if they want to completely remove all user data
|
; Ask user if they want to completely remove all user data
|
||||||
|
|||||||
@@ -572,11 +572,12 @@ function AddChannelDialog({ selectedType, onSelectType, onClose, onChannelAdded
|
|||||||
const config: Record<string, unknown> = { ...configValues };
|
const config: Record<string, unknown> = { ...configValues };
|
||||||
const saveResult = await window.electron.ipcRenderer.invoke('channel:saveConfig', selectedType, config) as {
|
const saveResult = await window.electron.ipcRenderer.invoke('channel:saveConfig', selectedType, config) as {
|
||||||
success?: boolean;
|
success?: boolean;
|
||||||
|
error?: string;
|
||||||
warning?: string;
|
warning?: string;
|
||||||
pluginInstalled?: boolean;
|
pluginInstalled?: boolean;
|
||||||
};
|
};
|
||||||
if (!saveResult?.success) {
|
if (!saveResult?.success) {
|
||||||
throw new Error('Failed to save channel config');
|
throw new Error(saveResult?.error || 'Failed to save channel config');
|
||||||
}
|
}
|
||||||
if (typeof saveResult.warning === 'string' && saveResult.warning) {
|
if (typeof saveResult.warning === 'string' && saveResult.warning) {
|
||||||
toast.warning(saveResult.warning);
|
toast.warning(saveResult.warning);
|
||||||
|
|||||||
Reference in New Issue
Block a user