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:
84
electron/utils/config.ts
Normal file
84
electron/utils/config.ts
Normal 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
133
electron/utils/logger.ts
Normal 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
72
electron/utils/paths.ts
Normal 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
123
electron/utils/store.ts
Normal 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');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user