/** * Path Utilities * Cross-platform path resolution helpers */ import { createRequire } from 'node:module'; import { dirname, isAbsolute, join, normalize } from 'path'; import { homedir } from 'os'; import { cpSync, existsSync, mkdirSync, readFileSync, realpathSync, rmSync } from 'fs'; const require = createRequire(import.meta.url); type ElectronAppLike = Pick; type OpenClawRuntimeSource = 'dev' | 'external' | 'managed' | 'bundled' | 'missing'; type OpenClawRuntimeResolution = { dir: string; source: OpenClawRuntimeSource; version?: string; bundledDir?: string; managedDir?: string; installedFromBundled?: boolean; }; let cachedOpenClawRuntime: OpenClawRuntimeResolution | null = null; // Modules that Zhinian loads from the OpenClaw package context at main-process // module initialization time. Some user-installed or previously managed // OpenClaw packages are valid CLIs but do not include these app-side // integration dependencies; selecting them would crash before the UI opens. const REQUIRED_OPENCLAW_CONTEXT_MODULES = [ '@whiskeysockets/baileys/package.json', 'qrcode-terminal/package.json', 'qrcode-terminal/vendor/QRCode/index.js', 'qrcode-terminal/vendor/QRCode/QRErrorCorrectLevel.js', ] as const; export { quoteForCmd, needsWinShell, prepareWinSpawn, normalizeNodeRequirePathForNodeOptions, appendNodeRequireToNodeOptions, } from './win-shell'; function getElectronApp() { if (process.versions?.electron) { return (require('electron') as typeof import('electron')).app; } const fallbackUserData = process.env.CLAWX_USER_DATA_DIR?.trim() || join(homedir(), '.clawx'); const fallbackAppPath = process.cwd(); const fallbackApp: ElectronAppLike = { isPackaged: false, getPath: (name) => { if (name === 'userData') return fallbackUserData; return fallbackUserData; }, getAppPath: () => fallbackAppPath, }; return fallbackApp; } /** * Expand ~ to home directory */ export function expandPath(path: string): string { if (path.startsWith('~')) { return path.replace('~', homedir()); } return path; } /** * Get OpenClaw config directory */ export function getOpenClawConfigDir(): string { return join(homedir(), '.openclaw'); } /** * Get OpenClaw skills directory */ export function getOpenClawSkillsDir(): string { return join(getOpenClawConfigDir(), 'skills'); } /** * Get ClawX config directory */ export function getClawXConfigDir(): string { return join(homedir(), '.clawx'); } /** * Get ClawX logs directory */ export function getLogsDir(): string { return join(getElectronApp().getPath('userData'), 'logs'); } /** * Get ClawX data directory */ export function getDataDir(): string { return getElectronApp().getPath('userData'); } /** * Ensure directory exists */ export function ensureDir(dir: string): void { if (!existsSync(dir)) { mkdirSync(dir, { recursive: true }); } } function fsPath(filePath: string): string { if (process.platform !== 'win32') return filePath; if (!filePath) return filePath; if (filePath.startsWith('\\\\?\\')) return filePath; const windowsPath = filePath.replace(/\//g, '\\'); if (!isAbsolute(windowsPath)) return windowsPath; if (windowsPath.startsWith('\\\\')) { return `\\\\?\\UNC\\${windowsPath.slice(2)}`; } return `\\\\?\\${windowsPath}`; } function normalizeCandidatePath(dir: string): string { return normalize(expandPath(dir.trim())); } function readOpenClawVersion(dir: string): string | undefined { try { const pkgPath = join(dir, 'package.json'); if (existsSync(fsPath(pkgPath))) { const pkg = JSON.parse(readFileSync(fsPath(pkgPath), 'utf-8')) as { version?: string }; return pkg.version; } } catch { // Ignore version read errors. } return undefined; } function isValidOpenClawPackageDir(dir: string): boolean { return Boolean(dir) && existsSync(fsPath(dir)) && existsSync(fsPath(join(dir, 'package.json'))) && existsSync(fsPath(join(dir, 'openclaw.mjs'))); } function hasRequiredOpenClawContextModules(dir: string): boolean { if (!isValidOpenClawPackageDir(dir)) return false; try { const runtimeRequire = createRequire(join(realpathSync(fsPath(dir)), 'package.json')); for (const specifier of REQUIRED_OPENCLAW_CONTEXT_MODULES) { runtimeRequire.resolve(specifier); } return true; } catch { try { const runtimeRequire = createRequire(join(dir, 'package.json')); for (const specifier of REQUIRED_OPENCLAW_CONTEXT_MODULES) { runtimeRequire.resolve(specifier); } return true; } catch { return false; } } } function samePath(left: string, right: string): boolean { try { return realpathSync(fsPath(left)) === realpathSync(fsPath(right)); } catch { return normalize(left) === normalize(right); } } function logOpenClawRuntime(message: string, extra?: unknown): void { try { const { logger } = require('./logger') as typeof import('./logger'); if (extra === undefined) { logger.info(message); } else { logger.info(message, extra); } } catch { // Logger may be unavailable in unit tests or non-Electron contexts. } } function getBundledOpenClawDir(): string { if (getElectronApp().isPackaged) { return join(process.resourcesPath, 'openclaw'); } return join(__dirname, '../../node_modules/openclaw'); } function getManagedOpenClawDir(): string { return join(getOpenClawConfigDir(), 'runtime', 'openclaw'); } function getEnvOpenClawCandidates(): string[] { return [ process.env.YINIAN_OPENCLAW_DIR, process.env.CLAWX_OPENCLAW_DIR, process.env.OPENCLAW_DIR, ].filter((value): value is string => Boolean(value?.trim())); } function getStandardOpenClawCandidates(): string[] { const home = homedir(); const candidates = [ join(home, '.openclaw', 'openclaw'), join(home, '.openclaw', 'node_modules', 'openclaw'), join(home, '.npm-global', 'lib', 'node_modules', 'openclaw'), join(home, '.local', 'lib', 'node_modules', 'openclaw'), ]; if (process.platform === 'darwin') { candidates.push( '/usr/local/lib/node_modules/openclaw', '/opt/homebrew/lib/node_modules/openclaw', ); } else if (process.platform === 'linux') { candidates.push( '/usr/local/lib/node_modules/openclaw', '/usr/lib/node_modules/openclaw', ); } else if (process.platform === 'win32') { const appData = process.env.APPDATA; const programFiles = process.env.ProgramFiles; const programFilesX86 = process.env['ProgramFiles(x86)']; if (appData) candidates.push(join(appData, 'npm', 'node_modules', 'openclaw')); if (programFiles) candidates.push(join(programFiles, 'nodejs', 'node_modules', 'openclaw')); if (programFilesX86) candidates.push(join(programFilesX86, 'nodejs', 'node_modules', 'openclaw')); } return candidates; } function findExternalOpenClawDir(excludedDirs: string[]): string | null { const seen = new Set(); const candidates = [...getEnvOpenClawCandidates(), ...getStandardOpenClawCandidates()]; for (const rawCandidate of candidates) { const candidate = normalizeCandidatePath(rawCandidate); if (seen.has(candidate)) continue; seen.add(candidate); if (excludedDirs.some((excluded) => samePath(candidate, excluded))) continue; if (!isValidOpenClawPackageDir(candidate)) continue; if (hasRequiredOpenClawContextModules(candidate)) return candidate; logOpenClawRuntime('[openclaw-runtime] Ignoring external OpenClaw installation because required app dependencies are missing', { candidate, requiredModules: REQUIRED_OPENCLAW_CONTEXT_MODULES, }); } return null; } function installBundledOpenClawToManagedRuntime(bundledDir: string, managedDir: string): boolean { if (!isValidOpenClawPackageDir(bundledDir)) return false; const bundledVersion = readOpenClawVersion(bundledDir); const managedVersion = isValidOpenClawPackageDir(managedDir) ? readOpenClawVersion(managedDir) : undefined; if (managedVersion && bundledVersion && managedVersion === bundledVersion && hasRequiredOpenClawContextModules(managedDir)) { return false; } const tempDir = `${managedDir}.tmp-${Date.now()}`; rmSync(fsPath(tempDir), { recursive: true, force: true }); mkdirSync(fsPath(dirname(tempDir)), { recursive: true }); cpSync(fsPath(bundledDir), fsPath(tempDir), { recursive: true, dereference: true }); rmSync(fsPath(managedDir), { recursive: true, force: true }); cpSync(fsPath(tempDir), fsPath(managedDir), { recursive: true, dereference: true }); rmSync(fsPath(tempDir), { recursive: true, force: true }); return true; } function resolveOpenClawRuntime(): OpenClawRuntimeResolution { if (cachedOpenClawRuntime) return cachedOpenClawRuntime; const app = getElectronApp(); const bundledDir = getBundledOpenClawDir(); const managedDir = getManagedOpenClawDir(); if (!app.isPackaged) { cachedOpenClawRuntime = { dir: bundledDir, source: isValidOpenClawPackageDir(bundledDir) ? 'dev' : 'missing', version: readOpenClawVersion(bundledDir), bundledDir, managedDir, }; return cachedOpenClawRuntime; } if (hasRequiredOpenClawContextModules(managedDir)) { cachedOpenClawRuntime = { dir: managedDir, source: 'managed', version: readOpenClawVersion(managedDir), bundledDir, managedDir, }; logOpenClawRuntime('[openclaw-runtime] Using managed OpenClaw runtime', cachedOpenClawRuntime); return cachedOpenClawRuntime; } const externalDir = findExternalOpenClawDir([bundledDir, managedDir]); if (externalDir) { cachedOpenClawRuntime = { dir: externalDir, source: 'external', version: readOpenClawVersion(externalDir), bundledDir, managedDir, }; logOpenClawRuntime('[openclaw-runtime] Using existing OpenClaw installation before managed runtime is installed', cachedOpenClawRuntime); return cachedOpenClawRuntime; } let installedFromBundled = false; if (isValidOpenClawPackageDir(bundledDir)) { try { installedFromBundled = installBundledOpenClawToManagedRuntime(bundledDir, managedDir); } catch (error) { logOpenClawRuntime('[openclaw-runtime] Failed to install bundled OpenClaw runtime, falling back to bundled resources', error); } } if (hasRequiredOpenClawContextModules(managedDir)) { cachedOpenClawRuntime = { dir: managedDir, source: 'managed', version: readOpenClawVersion(managedDir), bundledDir, managedDir, installedFromBundled, }; logOpenClawRuntime( installedFromBundled ? '[openclaw-runtime] Installed bundled OpenClaw runtime' : '[openclaw-runtime] Using managed OpenClaw runtime', cachedOpenClawRuntime, ); return cachedOpenClawRuntime; } if (isValidOpenClawPackageDir(managedDir)) { logOpenClawRuntime('[openclaw-runtime] Ignoring managed OpenClaw runtime because required app dependencies are missing', { managedDir, requiredModules: REQUIRED_OPENCLAW_CONTEXT_MODULES, }); } cachedOpenClawRuntime = { dir: bundledDir, source: isValidOpenClawPackageDir(bundledDir) ? 'bundled' : 'missing', version: readOpenClawVersion(bundledDir), bundledDir, managedDir, }; return cachedOpenClawRuntime; } /** * Get resources directory (for bundled assets) */ export function getResourcesDir(): string { if (getElectronApp().isPackaged) { return join(process.resourcesPath, 'resources'); } return join(__dirname, '../../resources'); } /** * Get preload script path */ export function getPreloadPath(): string { return join(__dirname, '../preload/index.js'); } /** * Get OpenClaw package directory. * * Runtime resolution policy: * 1. Development uses workspace node_modules/openclaw. * 2. Packaged builds first adapt an existing user/system OpenClaw package. * 3. If none exists, copy the bundled OpenClaw package into * ~/.openclaw/runtime/openclaw and run from there. * 4. If the managed install fails, fall back to packaged resources/openclaw. */ export function getOpenClawDir(): string { return resolveOpenClawRuntime().dir; } export function reinstallManagedOpenClawRuntime(): OpenClawRuntimeResolution { cachedOpenClawRuntime = null; const bundledDir = getBundledOpenClawDir(); const managedDir = getManagedOpenClawDir(); rmSync(fsPath(managedDir), { recursive: true, force: true }); let installedFromBundled = false; if (isValidOpenClawPackageDir(bundledDir)) { installedFromBundled = installBundledOpenClawToManagedRuntime(bundledDir, managedDir); } cachedOpenClawRuntime = { dir: hasRequiredOpenClawContextModules(managedDir) ? managedDir : bundledDir, source: hasRequiredOpenClawContextModules(managedDir) ? 'managed' : isValidOpenClawPackageDir(bundledDir) ? 'bundled' : 'missing', version: readOpenClawVersion(hasRequiredOpenClawContextModules(managedDir) ? managedDir : bundledDir), bundledDir, managedDir, installedFromBundled, }; logOpenClawRuntime('[openclaw-runtime] Reinstalled managed OpenClaw runtime for first-run initialization', cachedOpenClawRuntime); return cachedOpenClawRuntime; } /** * Get OpenClaw package directory resolved to a real path. * Useful when consumers need deterministic module resolution under pnpm symlinks. */ export function getOpenClawResolvedDir(): string { const dir = getOpenClawDir(); if (!existsSync(dir)) { return dir; } try { return realpathSync(dir); } catch { return dir; } } /** * Get OpenClaw entry script path (openclaw.mjs) */ export function getOpenClawEntryPath(): string { return join(getOpenClawDir(), 'openclaw.mjs'); } /** * Get ClawHub CLI entry script path (clawdhub.js) */ export function getClawHubCliEntryPath(): string { return join(getElectronApp().getAppPath(), 'node_modules', 'clawhub', 'bin', 'clawdhub.js'); } /** * Get ClawHub CLI binary path (node_modules/.bin) */ export function getClawHubCliBinPath(): string { const binName = process.platform === 'win32' ? 'clawhub.cmd' : 'clawhub'; return join(getElectronApp().getAppPath(), 'node_modules', '.bin', binName); } /** * Check if OpenClaw package exists */ export function isOpenClawPresent(): boolean { const dir = getOpenClawDir(); const pkgJsonPath = join(dir, 'package.json'); return existsSync(dir) && existsSync(pkgJsonPath); } /** * Check if OpenClaw is built (has dist folder) * For the npm package, this should always be true since npm publishes the built dist. */ export function isOpenClawBuilt(): boolean { const dir = getOpenClawDir(); const distDir = join(dir, 'dist'); const hasDist = existsSync(distDir); return hasDist; } /** * Get OpenClaw status for environment check */ export interface OpenClawStatus { packageExists: boolean; isBuilt: boolean; entryPath: string; dir: string; version?: string; source?: OpenClawRuntimeSource; bundledDir?: string; managedDir?: string; installedFromBundled?: boolean; } export function getOpenClawStatus(): OpenClawStatus { const runtime = resolveOpenClawRuntime(); const dir = runtime.dir; const status: OpenClawStatus = { packageExists: isOpenClawPresent(), isBuilt: isOpenClawBuilt(), entryPath: getOpenClawEntryPath(), dir, version: runtime.version, source: runtime.source, bundledDir: runtime.bundledDir, managedDir: runtime.managedDir, installedFromBundled: runtime.installedFromBundled, }; try { const { logger } = require('./logger') as typeof import('./logger'); logger.info('OpenClaw status:', status); } catch { // Ignore logger bootstrap issues in non-Electron contexts such as unit tests. } return status; }