feat(core): initialize project skeleton with Electron + React + TypeScript

Set up the complete project foundation for ClawX, a graphical AI assistant:

- Electron main process with IPC handlers, menu, tray, and gateway management
- React renderer with routing, layout components, and page scaffolding
- Zustand state management for gateway, settings, channels, skills, chat, and cron
- shadcn/ui components with Tailwind CSS and CSS variable theming
- Build tooling with Vite, electron-builder, and TypeScript configuration
- Testing setup with Vitest and Playwright
- Development configurations (ESLint, Prettier, gitignore, env example)
This commit is contained in:
Haze
2026-02-05 23:09:17 +08:00
Unverified
parent 9442e5f77a
commit b8ab0208d0
71 changed files with 14086 additions and 3 deletions

84
electron/utils/config.ts Normal file
View File

@@ -0,0 +1,84 @@
/**
* Application Configuration
* Centralized configuration constants and helpers
*/
/**
* Port configuration
*/
export const PORTS = {
/** ClawX GUI development server port */
CLAWX_DEV: 5173,
/** ClawX GUI production port (for reference) */
CLAWX_GUI: 23333,
/** OpenClaw Gateway port */
OPENCLAW_GATEWAY: 18789,
} as const;
/**
* Get port from environment or default
*/
export function getPort(key: keyof typeof PORTS): number {
const envKey = `CLAWX_PORT_${key}`;
const envValue = process.env[envKey];
return envValue ? parseInt(envValue, 10) : PORTS[key];
}
/**
* Application paths
*/
export const APP_PATHS = {
/** OpenClaw configuration directory */
OPENCLAW_CONFIG: '~/.openclaw',
/** ClawX configuration directory */
CLAWX_CONFIG: '~/.clawx',
/** Log files directory */
LOGS: '~/.clawx/logs',
} as const;
/**
* Update channels
*/
export const UPDATE_CHANNELS = ['stable', 'beta', 'dev'] as const;
export type UpdateChannel = (typeof UPDATE_CHANNELS)[number];
/**
* Default update configuration
*/
export const UPDATE_CONFIG = {
/** Check interval in milliseconds (6 hours) */
CHECK_INTERVAL: 6 * 60 * 60 * 1000,
/** Default update channel */
DEFAULT_CHANNEL: 'stable' as UpdateChannel,
/** Auto download updates */
AUTO_DOWNLOAD: false,
/** Show update notifications */
SHOW_NOTIFICATION: true,
};
/**
* Gateway configuration
*/
export const GATEWAY_CONFIG = {
/** WebSocket reconnection delay (ms) */
RECONNECT_DELAY: 5000,
/** RPC call timeout (ms) */
RPC_TIMEOUT: 30000,
/** Health check interval (ms) */
HEALTH_CHECK_INTERVAL: 30000,
/** Maximum startup retries */
MAX_STARTUP_RETRIES: 30,
/** Startup retry interval (ms) */
STARTUP_RETRY_INTERVAL: 1000,
};

133
electron/utils/logger.ts Normal file
View File

@@ -0,0 +1,133 @@
/**
* Logger Utility
* Centralized logging with levels and file output
*/
import { app } from 'electron';
import { join } from 'path';
import { existsSync, mkdirSync, appendFileSync } from 'fs';
/**
* Log levels
*/
export enum LogLevel {
DEBUG = 0,
INFO = 1,
WARN = 2,
ERROR = 3,
}
/**
* Current log level (can be changed at runtime)
*/
let currentLevel = LogLevel.INFO;
/**
* Log file path
*/
let logFilePath: string | null = null;
/**
* Initialize logger
*/
export function initLogger(): void {
try {
const 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`);
} catch (error) {
console.error('Failed to initialize logger:', error);
}
}
/**
* Set log level
*/
export function setLogLevel(level: LogLevel): void {
currentLevel = level;
}
/**
* 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}`;
}
/**
* Write to log file
*/
function writeToFile(formatted: string): void {
if (logFilePath) {
try {
appendFileSync(logFilePath, formatted + '\n');
} catch (error) {
// Silently fail if we can't write to file
}
}
}
/**
* Log debug message
*/
export function debug(message: string, ...args: unknown[]): void {
if (currentLevel <= LogLevel.DEBUG) {
const formatted = formatMessage('DEBUG', message, ...args);
console.debug(formatted);
writeToFile(formatted);
}
}
/**
* Log info message
*/
export function info(message: string, ...args: unknown[]): void {
if (currentLevel <= LogLevel.INFO) {
const formatted = formatMessage('INFO', message, ...args);
console.info(formatted);
writeToFile(formatted);
}
}
/**
* Log warning message
*/
export function warn(message: string, ...args: unknown[]): void {
if (currentLevel <= LogLevel.WARN) {
const formatted = formatMessage('WARN', message, ...args);
console.warn(formatted);
writeToFile(formatted);
}
}
/**
* Log error message
*/
export function error(message: string, ...args: unknown[]): void {
if (currentLevel <= LogLevel.ERROR) {
const formatted = formatMessage('ERROR', message, ...args);
console.error(formatted);
writeToFile(formatted);
}
}
/**
* Logger namespace export
*/
export const logger = {
debug,
info,
warn,
error,
setLevel: setLogLevel,
init: initLogger,
};

72
electron/utils/paths.ts Normal file
View File

@@ -0,0 +1,72 @@
/**
* Path Utilities
* Cross-platform path resolution helpers
*/
import { app } from 'electron';
import { join } from 'path';
import { homedir } from 'os';
import { existsSync, mkdirSync } from 'fs';
/**
* 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 ClawX config directory
*/
export function getClawXConfigDir(): string {
return join(homedir(), '.clawx');
}
/**
* Get ClawX logs directory
*/
export function getLogsDir(): string {
return join(app.getPath('userData'), 'logs');
}
/**
* Get ClawX data directory
*/
export function getDataDir(): string {
return app.getPath('userData');
}
/**
* Ensure directory exists
*/
export function ensureDir(dir: string): void {
if (!existsSync(dir)) {
mkdirSync(dir, { recursive: true });
}
}
/**
* Get resources directory (for bundled assets)
*/
export function getResourcesDir(): string {
if (app.isPackaged) {
return join(process.resourcesPath, 'resources');
}
return join(__dirname, '../../resources');
}
/**
* Get preload script path
*/
export function getPreloadPath(): string {
return join(__dirname, '../preload/index.js');
}

123
electron/utils/store.ts Normal file
View File

@@ -0,0 +1,123 @@
/**
* Persistent Storage
* Electron-store wrapper for application settings
*/
import Store from 'electron-store';
/**
* Application settings schema
*/
export interface AppSettings {
// General
theme: 'light' | 'dark' | 'system';
language: string;
startMinimized: boolean;
launchAtStartup: boolean;
// Gateway
gatewayAutoStart: boolean;
gatewayPort: number;
// Update
updateChannel: 'stable' | 'beta' | 'dev';
autoCheckUpdate: boolean;
autoDownloadUpdate: boolean;
skippedVersions: string[];
// UI State
sidebarCollapsed: boolean;
devModeUnlocked: boolean;
// Presets
selectedBundles: string[];
enabledSkills: string[];
disabledSkills: string[];
}
/**
* Default settings
*/
const defaults: AppSettings = {
// General
theme: 'system',
language: 'en',
startMinimized: false,
launchAtStartup: false,
// Gateway
gatewayAutoStart: true,
gatewayPort: 18789,
// Update
updateChannel: 'stable',
autoCheckUpdate: true,
autoDownloadUpdate: false,
skippedVersions: [],
// UI State
sidebarCollapsed: false,
devModeUnlocked: false,
// Presets
selectedBundles: ['productivity', 'developer'],
enabledSkills: [],
disabledSkills: [],
};
/**
* Create settings store
*/
export const settingsStore = new Store<AppSettings>({
name: 'settings',
defaults,
});
/**
* Get a setting value
*/
export function getSetting<K extends keyof AppSettings>(key: K): AppSettings[K] {
return settingsStore.get(key);
}
/**
* Set a setting value
*/
export function setSetting<K extends keyof AppSettings>(
key: K,
value: AppSettings[K]
): void {
settingsStore.set(key, value);
}
/**
* Get all settings
*/
export function getAllSettings(): AppSettings {
return settingsStore.store;
}
/**
* Reset settings to defaults
*/
export function resetSettings(): void {
settingsStore.clear();
}
/**
* Export settings to JSON
*/
export function exportSettings(): string {
return JSON.stringify(settingsStore.store, null, 2);
}
/**
* Import settings from JSON
*/
export function importSettings(json: string): void {
try {
const settings = JSON.parse(json);
settingsStore.set(settings);
} catch (error) {
throw new Error('Invalid settings JSON');
}
}