/** * Path Utilities * Cross-platform path resolution helpers */ import { createRequire } from 'node:module'; import { basename, dirname, isAbsolute, join, normalize } from 'path'; import { homedir } from 'os'; import { cpSync, existsSync, mkdirSync, readFileSync, realpathSync, rmSync, symlinkSync } from 'fs'; import { cleanupOpenClawRuntimeNativeClipboard } from './optional-native-cleanup'; 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; const YINIAN_OPENCLAW_RUNTIME_PATCH_MARKER = '.yinian-runtime-patch.json'; const YINIAN_OPENCLAW_RUNTIME_PATCH_VERSION = '2026-05-12-runtime-templates-selfref-v1'; const REQUIRED_OPENCLAW_RUNTIME_TEMPLATE_FILES = [ 'SOUL.md', 'IDENTITY.md', 'USER.md', 'AGENTS.md', 'TOOLS.md', 'HEARTBEAT.md', 'BOOT.md', ] 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 readYinianOpenClawRuntimePatchVersion(dir: string): string | undefined { try { const markerPath = join(dir, YINIAN_OPENCLAW_RUNTIME_PATCH_MARKER); if (!existsSync(fsPath(markerPath))) return undefined; const marker = JSON.parse(readFileSync(fsPath(markerPath), 'utf-8')) as { version?: string }; return marker.version; } catch { return undefined; } } function hasYinianOpenClawRuntimePatch(dir: string): boolean { return readYinianOpenClawRuntimePatchVersion(dir) === YINIAN_OPENCLAW_RUNTIME_PATCH_VERSION; } function hasRequiredOpenClawRuntimeTemplates(dir: string): boolean { return REQUIRED_OPENCLAW_RUNTIME_TEMPLATE_FILES.every((fileName) => ( existsSync(fsPath(join(dir, 'docs', 'reference', 'templates', fileName))) )); } function isNodeModulesPackagePath(dir: string): boolean { try { const realDir = realpathSync(fsPath(dir)); return basename(realDir) === 'openclaw' && basename(dirname(realDir)) === 'node_modules'; } catch { return basename(dir) === 'openclaw' && basename(dirname(dir)) === 'node_modules'; } } function hasOpenClawSelfReference(dir: string): boolean { if (isNodeModulesPackagePath(dir)) return true; const selfRefDir = join(dir, 'node_modules', 'openclaw'); if (!existsSync(fsPath(join(selfRefDir, 'package.json')))) return false; try { return realpathSync(fsPath(selfRefDir)) === realpathSync(fsPath(dir)); } catch { return false; } } function ensureOpenClawSelfReference(dir: string): void { if (!isValidOpenClawPackageDir(dir) || hasOpenClawSelfReference(dir)) return; const nodeModulesDir = join(dir, 'node_modules'); const selfRefDir = join(nodeModulesDir, 'openclaw'); try { rmSync(fsPath(selfRefDir), { recursive: true, force: true }); mkdirSync(fsPath(nodeModulesDir), { recursive: true }); symlinkSync( process.platform === 'win32' ? fsPath(dir) : '..', fsPath(selfRefDir), process.platform === 'win32' ? 'junction' : 'dir', ); } catch (error) { logOpenClawRuntime('[openclaw-runtime] Failed to create OpenClaw self-reference for managed runtime', { dir, error: error instanceof Error ? error.message : String(error), }); } } function hasPackagedOpenClawRuntimeRequirements(dir: string): boolean { return hasRequiredOpenClawContextModules(dir) && hasRequiredOpenClawRuntimeTemplates(dir) && hasOpenClawSelfReference(dir) && hasYinianOpenClawRuntimePatch(dir); } 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 (hasPackagedOpenClawRuntimeRequirements(candidate)) return candidate; logOpenClawRuntime('[openclaw-runtime] Ignoring external OpenClaw installation because it is not a patched Yinian runtime', { candidate, requiredModules: REQUIRED_OPENCLAW_CONTEXT_MODULES, runtimePatchVersion: readYinianOpenClawRuntimePatchVersion(candidate) ?? null, expectedRuntimePatchVersion: YINIAN_OPENCLAW_RUNTIME_PATCH_VERSION, }); } 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 && hasPackagedOpenClawRuntimeRequirements(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 }); const removedClipboardPackages = cleanupOpenClawRuntimeNativeClipboard(tempDir); if (removedClipboardPackages > 0) { logOpenClawRuntime('[openclaw-runtime] Removed optional native clipboard packages from managed runtime', { removedClipboardPackages, }); } rmSync(fsPath(managedDir), { recursive: true, force: true }); cpSync(fsPath(tempDir), fsPath(managedDir), { recursive: true, dereference: true }); rmSync(fsPath(tempDir), { recursive: true, force: true }); ensureOpenClawSelfReference(managedDir); 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 (hasPackagedOpenClawRuntimeRequirements(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 (hasPackagedOpenClawRuntimeRequirements(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 it is not a patched Yinian runtime', { managedDir, requiredModules: REQUIRED_OPENCLAW_CONTEXT_MODULES, requiredTemplates: REQUIRED_OPENCLAW_RUNTIME_TEMPLATE_FILES, hasRuntimeTemplates: hasRequiredOpenClawRuntimeTemplates(managedDir), hasSelfReference: hasOpenClawSelfReference(managedDir), runtimePatchVersion: readYinianOpenClawRuntimePatchVersion(managedDir) ?? null, expectedRuntimePatchVersion: YINIAN_OPENCLAW_RUNTIME_PATCH_VERSION, }); } 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); } const managedReady = hasPackagedOpenClawRuntimeRequirements(managedDir); cachedOpenClawRuntime = { dir: managedReady ? managedDir : bundledDir, source: managedReady ? 'managed' : isValidOpenClawPackageDir(bundledDir) ? 'bundled' : 'missing', version: readOpenClawVersion(managedReady ? 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; }