import fs from 'fs/promises' import path from 'path' import { resolveNotebookPath } from '../../utils/pathSafety.js' import type { RemoteConfig, RemoteDevice } from '../../../shared/modules/remote/types.js' export interface RemoteServiceDependencies { } const REMOTE_DIR = 'remote' export interface DeviceData { lastConnected?: string } export class RemoteService { constructor(private deps: RemoteServiceDependencies = {}) { } private getRemoteDir(): { relPath: string; fullPath: string } { const { fullPath } = resolveNotebookPath(REMOTE_DIR) return { relPath: REMOTE_DIR, fullPath } } private getDeviceDir(deviceName: string): { relPath: string; fullPath: string } { const safeName = this.sanitizeFileName(deviceName) const { fullPath } = resolveNotebookPath(path.join(REMOTE_DIR, safeName)) return { relPath: path.join(REMOTE_DIR, safeName), fullPath } } private sanitizeFileName(name: string): string { return name.replace(/[<>:"/\\|?*]/g, '_').trim() || 'unnamed' } private getDeviceConfigPath(deviceName: string): string { const { fullPath } = this.getDeviceDir(deviceName) return path.join(fullPath, 'config.json') } private getDeviceScreenshotPath(deviceName: string): string { const { fullPath } = this.getDeviceDir(deviceName) return path.join(fullPath, 'screenshot.png') } private getDeviceDataPath(deviceName: string): string { const { fullPath } = this.getDeviceDir(deviceName) return path.join(fullPath, 'data.json') } private async ensureDir(dirPath: string): Promise { await fs.mkdir(dirPath, { recursive: true }) } private async getDeviceNames(): Promise { const { fullPath } = this.getRemoteDir() try { const entries = await fs.readdir(fullPath, { withFileTypes: true }) const dirs = entries.filter(e => e.isDirectory()).map(e => e.name) return dirs } catch { return [] } } async getConfig(): Promise { const deviceNames = await this.getDeviceNames() const devices: RemoteDevice[] = await Promise.all( deviceNames.map(async (name) => { try { const configPath = this.getDeviceConfigPath(name) const content = await fs.readFile(configPath, 'utf-8') const deviceConfig = JSON.parse(content) return { id: deviceConfig.id || name, deviceName: name, serverHost: deviceConfig.serverHost || '', desktopPort: deviceConfig.desktopPort || 3000, gitPort: deviceConfig.gitPort || 3001, } } catch { return { id: name, deviceName: name, serverHost: '', desktopPort: 3000, gitPort: 3001, } } }) ) return { devices } } async saveConfig(config: RemoteConfig): Promise { const { fullPath: remoteDirFullPath } = this.getRemoteDir() await this.ensureDir(remoteDirFullPath) const existingDevices = await this.getDeviceNames() const newDeviceNames = config.devices.map(d => this.sanitizeFileName(d.deviceName)) for (const oldDevice of existingDevices) { if (!newDeviceNames.includes(oldDevice)) { try { const oldDir = path.join(remoteDirFullPath, oldDevice) await fs.rm(oldDir, { recursive: true, force: true }) } catch { } } } for (const device of config.devices) { const deviceDir = this.getDeviceDir(device.deviceName) await this.ensureDir(deviceDir.fullPath) const deviceConfigPath = this.getDeviceConfigPath(device.deviceName) const deviceConfig = { id: device.id, serverHost: device.serverHost, desktopPort: device.desktopPort, gitPort: device.gitPort, } await fs.writeFile(deviceConfigPath, JSON.stringify(deviceConfig, null, 2), 'utf-8') } } async getScreenshot(deviceName?: string): Promise { if (!deviceName) { return null } const screenshotPath = this.getDeviceScreenshotPath(deviceName) try { return await fs.readFile(screenshotPath) } catch { return null } } async saveScreenshot(dataUrl: string, deviceName?: string): Promise { console.log('[RemoteService] saveScreenshot:', { deviceName, dataUrlLength: dataUrl?.length }) if (!deviceName || deviceName.trim() === '') { console.warn('[RemoteService] saveScreenshot skipped: no deviceName') return } const deviceDir = this.getDeviceDir(deviceName) await this.ensureDir(deviceDir.fullPath) const base64Data = dataUrl.replace(/^data:image\/png;base64,/, '') const buffer = Buffer.from(base64Data, 'base64') const screenshotPath = this.getDeviceScreenshotPath(deviceName) await fs.writeFile(screenshotPath, buffer) } async getData(deviceName?: string): Promise { if (!deviceName || deviceName.trim() === '') { return null } const dataPath = this.getDeviceDataPath(deviceName) try { const content = await fs.readFile(dataPath, 'utf-8') return JSON.parse(content) } catch { return null } } async saveData(data: DeviceData, deviceName?: string): Promise { if (!deviceName || deviceName.trim() === '') { console.warn('[RemoteService] saveData skipped: no deviceName') return } const deviceDir = this.getDeviceDir(deviceName) await this.ensureDir(deviceDir.fullPath) const dataPath = this.getDeviceDataPath(deviceName) await fs.writeFile(dataPath, JSON.stringify(data, null, 2), 'utf-8') } } export const createRemoteService = (deps?: RemoteServiceDependencies): RemoteService => { return new RemoteService(deps) }