refactor: restructure to monorepo with apps and packages directories - Move companion app to apps/companion with electron-vite - Move domain/application/infrastructure to packages/ - Fix ELECTRON_RUN_AS_NODE env var issue for VS Code terminal - Remove legacy esbuild bundler (replaced by electron-vite) - Update workspace scripts in root package.json
This commit is contained in:
142
apps/companion/main/di-container.ts
Normal file
142
apps/companion/main/di-container.ts
Normal file
@@ -0,0 +1,142 @@
|
||||
import { InMemorySessionRepository } from '@/packages/infrastructure/repositories/InMemorySessionRepository';
|
||||
import { MockBrowserAutomationAdapter } from '@/packages/infrastructure/adapters/automation/MockBrowserAutomationAdapter';
|
||||
import { BrowserDevToolsAdapter } from '@/packages/infrastructure/adapters/automation/BrowserDevToolsAdapter';
|
||||
import { NutJsAutomationAdapter } from '@/packages/infrastructure/adapters/automation/NutJsAutomationAdapter';
|
||||
import { MockAutomationEngineAdapter } from '@/packages/infrastructure/adapters/automation/MockAutomationEngineAdapter';
|
||||
import { StartAutomationSessionUseCase } from '@/packages/application/use-cases/StartAutomationSessionUseCase';
|
||||
import { loadAutomationConfig, AutomationMode } from '@/packages/infrastructure/config';
|
||||
import type { ISessionRepository } from '@/packages/application/ports/ISessionRepository';
|
||||
import type { IBrowserAutomation } from '@/packages/application/ports/IBrowserAutomation';
|
||||
import type { IAutomationEngine } from '@/packages/application/ports/IAutomationEngine';
|
||||
|
||||
export interface BrowserConnectionResult {
|
||||
success: boolean;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create browser automation adapter based on configuration mode.
|
||||
*
|
||||
* @param mode - The automation mode from configuration
|
||||
* @returns IBrowserAutomation adapter instance
|
||||
*/
|
||||
function createBrowserAutomationAdapter(mode: AutomationMode): IBrowserAutomation {
|
||||
const config = loadAutomationConfig();
|
||||
|
||||
switch (mode) {
|
||||
case 'dev':
|
||||
return new BrowserDevToolsAdapter({
|
||||
debuggingPort: config.devTools?.debuggingPort,
|
||||
browserWSEndpoint: config.devTools?.browserWSEndpoint,
|
||||
defaultTimeout: config.defaultTimeout,
|
||||
});
|
||||
|
||||
case 'production':
|
||||
return new NutJsAutomationAdapter({
|
||||
mouseSpeed: config.nutJs?.mouseSpeed,
|
||||
keyboardDelay: config.nutJs?.keyboardDelay,
|
||||
defaultTimeout: config.defaultTimeout,
|
||||
});
|
||||
|
||||
case 'mock':
|
||||
default:
|
||||
return new MockBrowserAutomationAdapter();
|
||||
}
|
||||
}
|
||||
|
||||
export class DIContainer {
|
||||
private static instance: DIContainer;
|
||||
|
||||
private sessionRepository: ISessionRepository;
|
||||
private browserAutomation: IBrowserAutomation;
|
||||
private automationEngine: IAutomationEngine;
|
||||
private startAutomationUseCase: StartAutomationSessionUseCase;
|
||||
private automationMode: AutomationMode;
|
||||
|
||||
private constructor() {
|
||||
const config = loadAutomationConfig();
|
||||
this.automationMode = config.mode;
|
||||
|
||||
this.sessionRepository = new InMemorySessionRepository();
|
||||
this.browserAutomation = createBrowserAutomationAdapter(config.mode);
|
||||
this.automationEngine = new MockAutomationEngineAdapter(
|
||||
this.browserAutomation,
|
||||
this.sessionRepository
|
||||
);
|
||||
this.startAutomationUseCase = new StartAutomationSessionUseCase(
|
||||
this.automationEngine,
|
||||
this.browserAutomation,
|
||||
this.sessionRepository
|
||||
);
|
||||
}
|
||||
|
||||
public static getInstance(): DIContainer {
|
||||
if (!DIContainer.instance) {
|
||||
DIContainer.instance = new DIContainer();
|
||||
}
|
||||
return DIContainer.instance;
|
||||
}
|
||||
|
||||
public getStartAutomationUseCase(): StartAutomationSessionUseCase {
|
||||
return this.startAutomationUseCase;
|
||||
}
|
||||
|
||||
public getSessionRepository(): ISessionRepository {
|
||||
return this.sessionRepository;
|
||||
}
|
||||
|
||||
public getAutomationEngine(): IAutomationEngine {
|
||||
return this.automationEngine;
|
||||
}
|
||||
|
||||
public getAutomationMode(): AutomationMode {
|
||||
return this.automationMode;
|
||||
}
|
||||
|
||||
public getBrowserAutomation(): IBrowserAutomation {
|
||||
return this.browserAutomation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize browser connection for dev mode.
|
||||
* In dev mode, connects to the browser via Chrome DevTools Protocol.
|
||||
* In mock mode, returns success immediately (no connection needed).
|
||||
*/
|
||||
public async initializeBrowserConnection(): Promise<BrowserConnectionResult> {
|
||||
if (this.automationMode === 'dev') {
|
||||
try {
|
||||
const devToolsAdapter = this.browserAutomation as BrowserDevToolsAdapter;
|
||||
await devToolsAdapter.connect();
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to connect to browser'
|
||||
};
|
||||
}
|
||||
}
|
||||
if (this.automationMode === 'production') {
|
||||
try {
|
||||
const nutJsAdapter = this.browserAutomation as NutJsAutomationAdapter;
|
||||
const result = await nutJsAdapter.connect();
|
||||
if (!result.success) {
|
||||
return { success: false, error: result.error };
|
||||
}
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to initialize nut.js'
|
||||
};
|
||||
}
|
||||
}
|
||||
return { success: true }; // Mock mode doesn't need connection
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the singleton instance (useful for testing with different configurations).
|
||||
*/
|
||||
public static resetInstance(): void {
|
||||
DIContainer.instance = undefined as unknown as DIContainer;
|
||||
}
|
||||
}
|
||||
41
apps/companion/main/index.ts
Normal file
41
apps/companion/main/index.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { app, BrowserWindow } from 'electron';
|
||||
import * as path from 'path';
|
||||
import { setupIpcHandlers } from './ipc-handlers';
|
||||
|
||||
function createWindow() {
|
||||
const mainWindow = new BrowserWindow({
|
||||
width: 1200,
|
||||
height: 800,
|
||||
webPreferences: {
|
||||
preload: path.join(__dirname, 'preload.js'),
|
||||
contextIsolation: true,
|
||||
nodeIntegration: false
|
||||
}
|
||||
});
|
||||
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
mainWindow.loadURL('http://localhost:5173');
|
||||
mainWindow.webContents.openDevTools();
|
||||
} else {
|
||||
// Path from dist/main to dist/renderer
|
||||
mainWindow.loadFile(path.join(__dirname, '../renderer/index.html'));
|
||||
}
|
||||
|
||||
setupIpcHandlers(mainWindow);
|
||||
}
|
||||
|
||||
app.whenReady().then(() => {
|
||||
createWindow();
|
||||
|
||||
app.on('activate', () => {
|
||||
if (BrowserWindow.getAllWindows().length === 0) {
|
||||
createWindow();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
app.on('window-all-closed', () => {
|
||||
if (process.platform !== 'darwin') {
|
||||
app.quit();
|
||||
}
|
||||
});
|
||||
111
apps/companion/main/ipc-handlers.ts
Normal file
111
apps/companion/main/ipc-handlers.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
import { ipcMain } from 'electron';
|
||||
import type { BrowserWindow, IpcMainInvokeEvent } from 'electron';
|
||||
import { DIContainer } from './di-container';
|
||||
import type { HostedSessionConfig } from '@/packages/domain/entities/HostedSessionConfig';
|
||||
import { StepId } from '@/packages/domain/value-objects/StepId';
|
||||
import { MockAutomationEngineAdapter } from '@/packages/infrastructure/adapters/automation/MockAutomationEngineAdapter';
|
||||
|
||||
export function setupIpcHandlers(mainWindow: BrowserWindow): void {
|
||||
const container = DIContainer.getInstance();
|
||||
const startAutomationUseCase = container.getStartAutomationUseCase();
|
||||
const sessionRepository = container.getSessionRepository();
|
||||
const automationEngine = container.getAutomationEngine();
|
||||
|
||||
ipcMain.handle('start-automation', async (_event: IpcMainInvokeEvent, config: HostedSessionConfig) => {
|
||||
try {
|
||||
// Connect to browser first (required for dev mode)
|
||||
const connectionResult = await container.initializeBrowserConnection();
|
||||
if (!connectionResult.success) {
|
||||
return { success: false, error: connectionResult.error };
|
||||
}
|
||||
|
||||
const result = await startAutomationUseCase.execute(config);
|
||||
const session = await sessionRepository.findById(result.sessionId);
|
||||
|
||||
if (session) {
|
||||
// Start the automation by executing step 1
|
||||
await automationEngine.executeStep(StepId.create(1), config);
|
||||
}
|
||||
|
||||
// Set up progress monitoring
|
||||
const checkInterval = setInterval(async () => {
|
||||
const updatedSession = await sessionRepository.findById(result.sessionId);
|
||||
if (!updatedSession) {
|
||||
clearInterval(checkInterval);
|
||||
return;
|
||||
}
|
||||
|
||||
mainWindow.webContents.send('session-progress', {
|
||||
sessionId: result.sessionId,
|
||||
currentStep: updatedSession.currentStep.value,
|
||||
state: updatedSession.state.value,
|
||||
completedSteps: Array.from({ length: updatedSession.currentStep.value - 1 }, (_, i) => i + 1),
|
||||
hasError: updatedSession.errorMessage !== undefined,
|
||||
errorMessage: updatedSession.errorMessage || null
|
||||
});
|
||||
|
||||
if (updatedSession.state.value === 'COMPLETED' ||
|
||||
updatedSession.state.value === 'FAILED' ||
|
||||
updatedSession.state.value === 'STOPPED_AT_STEP_18') {
|
||||
clearInterval(checkInterval);
|
||||
}
|
||||
}, 100);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
sessionId: result.sessionId
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error'
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('get-session-status', async (_event: IpcMainInvokeEvent, sessionId: string) => {
|
||||
const session = await sessionRepository.findById(sessionId);
|
||||
if (!session) {
|
||||
return { found: false };
|
||||
}
|
||||
|
||||
return {
|
||||
found: true,
|
||||
currentStep: session.currentStep.value,
|
||||
state: session.state.value,
|
||||
completedSteps: Array.from({ length: session.currentStep.value - 1 }, (_, i) => i + 1),
|
||||
hasError: session.errorMessage !== undefined,
|
||||
errorMessage: session.errorMessage || null
|
||||
};
|
||||
});
|
||||
|
||||
ipcMain.handle('pause-automation', async (_event: IpcMainInvokeEvent, _sessionId: string) => {
|
||||
return { success: false, error: 'Pause not implemented in POC' };
|
||||
});
|
||||
|
||||
ipcMain.handle('resume-automation', async (_event: IpcMainInvokeEvent, _sessionId: string) => {
|
||||
return { success: false, error: 'Resume not implemented in POC' };
|
||||
});
|
||||
|
||||
ipcMain.handle('stop-automation', async (_event: IpcMainInvokeEvent, sessionId: string) => {
|
||||
try {
|
||||
// Stop the automation engine interval
|
||||
const engine = automationEngine as MockAutomationEngineAdapter;
|
||||
engine.stopAutomation();
|
||||
|
||||
// Update session state to failed with user stop reason
|
||||
const session = await sessionRepository.findById(sessionId);
|
||||
if (session) {
|
||||
session.fail('User stopped automation');
|
||||
await sessionRepository.update(session);
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error'
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
22
apps/companion/main/preload.ts
Normal file
22
apps/companion/main/preload.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { contextBridge, ipcRenderer } from 'electron';
|
||||
import type { HostedSessionConfig } from '../../../packages/domain/entities/HostedSessionConfig';
|
||||
|
||||
export interface ElectronAPI {
|
||||
startAutomation: (config: HostedSessionConfig) => Promise<{ success: boolean; sessionId?: string; error?: string }>;
|
||||
stopAutomation: (sessionId: string) => Promise<{ success: boolean; error?: string }>;
|
||||
getSessionStatus: (sessionId: string) => Promise<any>;
|
||||
pauseAutomation: (sessionId: string) => Promise<{ success: boolean; error?: string }>;
|
||||
resumeAutomation: (sessionId: string) => Promise<{ success: boolean; error?: string }>;
|
||||
onSessionProgress: (callback: (progress: any) => void) => void;
|
||||
}
|
||||
|
||||
contextBridge.exposeInMainWorld('electronAPI', {
|
||||
startAutomation: (config: HostedSessionConfig) => ipcRenderer.invoke('start-automation', config),
|
||||
stopAutomation: (sessionId: string) => ipcRenderer.invoke('stop-automation', sessionId),
|
||||
getSessionStatus: (sessionId: string) => ipcRenderer.invoke('get-session-status', sessionId),
|
||||
pauseAutomation: (sessionId: string) => ipcRenderer.invoke('pause-automation', sessionId),
|
||||
resumeAutomation: (sessionId: string) => ipcRenderer.invoke('resume-automation', sessionId),
|
||||
onSessionProgress: (callback: (progress: any) => void) => {
|
||||
ipcRenderer.on('session-progress', (_event, progress) => callback(progress));
|
||||
}
|
||||
} as ElectronAPI);
|
||||
Reference in New Issue
Block a user