Chore/build npm (#9)

Co-authored-by: DigHuang <114602213+DigHuang@users.noreply.github.com>
Co-authored-by: Felix <24791380+vcfgv@users.noreply.github.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Haze
2026-02-09 15:10:08 +08:00
committed by GitHub
Unverified
parent 0b7f1c700e
commit de445ae3d5
37 changed files with 7359 additions and 1586 deletions

View File

@@ -1,10 +1,10 @@
/**
* Logger Utility
* Centralized logging with levels and file output
* Centralized logging with levels, file output, and log retrieval for UI
*/
import { app } from 'electron';
import { join } from 'path';
import { existsSync, mkdirSync, appendFileSync } from 'fs';
import { existsSync, mkdirSync, appendFileSync, readFileSync, readdirSync, statSync } from 'fs';
/**
* Log levels
@@ -19,26 +19,37 @@ export enum LogLevel {
/**
* Current log level (can be changed at runtime)
*/
let currentLevel = LogLevel.INFO;
let currentLevel = LogLevel.DEBUG; // Default to DEBUG for better diagnostics
/**
* Log file path
*/
let logFilePath: string | null = null;
let logDir: string | null = null;
/**
* Initialize logger
* In-memory ring buffer for recent logs (useful for UI display)
*/
const RING_BUFFER_SIZE = 500;
const recentLogs: string[] = [];
/**
* Initialize logger — safe to call before app.isReady()
*/
export function initLogger(): void {
try {
const logDir = join(app.getPath('userData'), 'logs');
logDir = join(app.getPath('userData'), 'logs');
if (!existsSync(logDir)) {
mkdirSync(logDir, { recursive: true });
}
const timestamp = new Date().toISOString().split('T')[0];
logFilePath = join(logDir, `clawx-${timestamp}.log`);
// Write a separator for new session
const sessionHeader = `\n${'='.repeat(80)}\n[${new Date().toISOString()}] === ClawX Session Start (v${app.getVersion()}) ===\n${'='.repeat(80)}\n`;
appendFileSync(logFilePath, sessionHeader);
} catch (error) {
console.error('Failed to initialize logger:', error);
}
@@ -51,22 +62,53 @@ export function setLogLevel(level: LogLevel): void {
currentLevel = level;
}
/**
* Get log file directory path
*/
export function getLogDir(): string | null {
return logDir;
}
/**
* Get current log file path
*/
export function getLogFilePath(): string | null {
return logFilePath;
}
/**
* Format log message
*/
function formatMessage(level: string, message: string, ...args: unknown[]): string {
const timestamp = new Date().toISOString();
const formattedArgs = args.length > 0 ? ' ' + args.map(arg =>
typeof arg === 'object' ? JSON.stringify(arg) : String(arg)
).join(' ') : '';
return `[${timestamp}] [${level}] ${message}${formattedArgs}`;
const formattedArgs = args.length > 0 ? ' ' + args.map(arg => {
if (arg instanceof Error) {
return `${arg.message}\n${arg.stack || ''}`;
}
if (typeof arg === 'object') {
try {
return JSON.stringify(arg, null, 2);
} catch {
return String(arg);
}
}
return String(arg);
}).join(' ') : '';
return `[${timestamp}] [${level.padEnd(5)}] ${message}${formattedArgs}`;
}
/**
* Write to log file
* Write to log file and ring buffer
*/
function writeToFile(formatted: string): void {
function writeLog(formatted: string): void {
// Ring buffer
recentLogs.push(formatted);
if (recentLogs.length > RING_BUFFER_SIZE) {
recentLogs.shift();
}
// File
if (logFilePath) {
try {
appendFileSync(logFilePath, formatted + '\n');
@@ -83,7 +125,7 @@ export function debug(message: string, ...args: unknown[]): void {
if (currentLevel <= LogLevel.DEBUG) {
const formatted = formatMessage('DEBUG', message, ...args);
console.debug(formatted);
writeToFile(formatted);
writeLog(formatted);
}
}
@@ -94,7 +136,7 @@ export function info(message: string, ...args: unknown[]): void {
if (currentLevel <= LogLevel.INFO) {
const formatted = formatMessage('INFO', message, ...args);
console.info(formatted);
writeToFile(formatted);
writeLog(formatted);
}
}
@@ -105,7 +147,7 @@ export function warn(message: string, ...args: unknown[]): void {
if (currentLevel <= LogLevel.WARN) {
const formatted = formatMessage('WARN', message, ...args);
console.warn(formatted);
writeToFile(formatted);
writeLog(formatted);
}
}
@@ -116,7 +158,66 @@ export function error(message: string, ...args: unknown[]): void {
if (currentLevel <= LogLevel.ERROR) {
const formatted = formatMessage('ERROR', message, ...args);
console.error(formatted);
writeToFile(formatted);
writeLog(formatted);
}
}
/**
* Get recent logs from ring buffer (for UI display)
* @param count Number of recent log lines to return (default: all)
* @param minLevel Minimum log level to include (default: DEBUG)
*/
export function getRecentLogs(count?: number, minLevel?: LogLevel): string[] {
const filtered = minLevel != null
? recentLogs.filter(line => {
if (minLevel <= LogLevel.DEBUG) return true;
if (minLevel === LogLevel.INFO) return !line.includes('] [DEBUG');
if (minLevel === LogLevel.WARN) return line.includes('] [WARN') || line.includes('] [ERROR');
return line.includes('] [ERROR');
})
: recentLogs;
return count ? filtered.slice(-count) : [...filtered];
}
/**
* Read the current day's log file content (last N lines)
*/
export function readLogFile(tailLines = 200): string {
if (!logFilePath || !existsSync(logFilePath)) {
return '(No log file found)';
}
try {
const content = readFileSync(logFilePath, 'utf-8');
const lines = content.split('\n');
if (lines.length <= tailLines) return content;
return lines.slice(-tailLines).join('\n');
} catch (err) {
return `(Failed to read log file: ${err})`;
}
}
/**
* List available log files
*/
export function listLogFiles(): Array<{ name: string; path: string; size: number; modified: string }> {
if (!logDir || !existsSync(logDir)) return [];
try {
return readdirSync(logDir)
.filter(f => f.endsWith('.log'))
.map(f => {
const fullPath = join(logDir!, f);
const stat = statSync(fullPath);
return {
name: f,
path: fullPath,
size: stat.size,
modified: stat.mtime.toISOString(),
};
})
.sort((a, b) => b.modified.localeCompare(a.modified));
} catch {
return [];
}
}
@@ -130,4 +231,9 @@ export const logger = {
error,
setLevel: setLogLevel,
init: initLogger,
getLogDir,
getLogFilePath,
getRecentLogs,
readLogFile,
listLogFiles,
};

View File

@@ -5,7 +5,8 @@
import { app } from 'electron';
import { join } from 'path';
import { homedir } from 'os';
import { existsSync, mkdirSync } from 'fs';
import { existsSync, mkdirSync, readFileSync } from 'fs';
import { logger } from './logger';
/**
* Expand ~ to home directory
@@ -72,13 +73,16 @@ export function getPreloadPath(): string {
}
/**
* Get OpenClaw submodule directory
* Get OpenClaw package directory
* - Production (packaged): from resources/openclaw (copied by electron-builder extraResources)
* - Development: from node_modules/openclaw
*/
export function getOpenClawDir(): string {
if (app.isPackaged) {
return join(process.resourcesPath, 'openclaw');
}
return join(__dirname, '../../openclaw');
// Development: use node_modules/openclaw
return join(__dirname, '../../node_modules/openclaw');
}
/**
@@ -89,44 +93,65 @@ export function getOpenClawEntryPath(): string {
}
/**
* Check if OpenClaw submodule exists
* Check if OpenClaw package exists
*/
export function isOpenClawSubmodulePresent(): boolean {
return existsSync(getOpenClawDir()) && existsSync(join(getOpenClawDir(), 'package.json'));
export function isOpenClawPresent(): boolean {
const dir = getOpenClawDir();
const pkgJsonPath = join(dir, 'package.json');
const exists = existsSync(dir) && existsSync(pkgJsonPath);
logger.debug(`isOpenClawPresent: dir=${dir}, exists=${exists}`);
return exists;
}
/**
* Check if OpenClaw is built (has dist folder with entry.js)
* 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 {
return existsSync(join(getOpenClawDir(), 'dist', 'entry.js'));
}
/**
* Check if OpenClaw has node_modules installed
*/
export function isOpenClawInstalled(): boolean {
return existsSync(join(getOpenClawDir(), 'node_modules'));
const dir = getOpenClawDir();
// Check for dist/entry.js or just the dist directory with JS files
const entryPath = join(dir, 'dist', 'entry.js');
const distDir = join(dir, 'dist');
const hasEntry = existsSync(entryPath);
const hasDist = existsSync(distDir);
logger.debug(`isOpenClawBuilt: distDir=${distDir}, hasDist=${hasDist}, hasEntry=${hasEntry}`);
return hasDist;
}
/**
* Get OpenClaw status for environment check
*/
export interface OpenClawStatus {
submoduleExists: boolean;
isInstalled: boolean;
packageExists: boolean;
isBuilt: boolean;
entryPath: string;
dir: string;
version?: string;
}
export function getOpenClawStatus(): OpenClawStatus {
const dir = getOpenClawDir();
return {
submoduleExists: isOpenClawSubmodulePresent(),
isInstalled: isOpenClawInstalled(),
let version: string | undefined;
// Try to read version from package.json
try {
const pkgPath = join(dir, 'package.json');
if (existsSync(pkgPath)) {
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
version = pkg.version;
}
} catch {
// Ignore version read errors
}
const status: OpenClawStatus = {
packageExists: isOpenClawPresent(),
isBuilt: isOpenClawBuilt(),
entryPath: getOpenClawEntryPath(),
dir,
version,
};
logger.info('OpenClaw status:', status);
return status;
}