import { contextBridge, ipcRenderer } from 'electron' import { electronAPI } from '@electron-toolkit/preload' import { readFileSync, rmSync } from 'fs' type LauncherAuthPayload = { appUrl?: string | null handoffToken?: string | null audienceApp?: string | null } type RemoteWorkspaceTarget = { host: string user?: string | null port?: number | null workspaceRoot?: string | null } type ModulePrebuiltArtifact = { type?: string | null url?: string | null fileName?: string | null checksumSha256?: string | null extractPath?: string | null required?: boolean | null } type RuntimeImageArtifact = { image?: string | null url?: string | null fileName?: string | null checksumSha256?: string | null required?: boolean | null } type ModuleInstallOptions = { prebuiltArtifacts?: ModulePrebuiltArtifact[] | null runtimeImages?: RuntimeImageArtifact[] | null } const decodeArgValue = (prefix: string): string | null => { const rawValue = process.argv.find((arg) => arg.startsWith(prefix))?.slice(prefix.length) if (!rawValue) return null try { return decodeURIComponent(rawValue) } catch { return rawValue } } const readLauncherAuthFile = (): LauncherAuthPayload | null => { const filePath = process.env.ATNMY_HANDOFF_FILE if (!filePath) { console.info('[lms-preload] launcher auth file env is empty') return null } try { const payload = JSON.parse(readFileSync(filePath, 'utf8')) as LauncherAuthPayload rmSync(filePath, { force: true }) console.info('[lms-preload] launcher auth file loaded', { hasHandoffToken: Boolean(payload.handoffToken), audienceApp: payload.audienceApp ?? null }) return payload } catch (error) { console.warn('[lms-preload] launcher auth file read failed', error) return null } } const launcherAuthFile = readLauncherAuthFile() const launcherAuth = { appUrl: launcherAuthFile?.appUrl ?? decodeArgValue('--url=') ?? process.env.ATNMY_APP_URL ?? null, handoffToken: launcherAuthFile?.handoffToken ?? decodeArgValue('--handoff-token=') ?? process.env.ATNMY_HANDOFF_TOKEN ?? null, audienceApp: launcherAuthFile?.audienceApp ?? decodeArgValue('--audience-app=') ?? process.env.ATNMY_AUDIENCE_APP ?? 'LMS' } console.info('[lms-preload] launcher auth resolved', { hasHandoffToken: Boolean(launcherAuth.handoffToken), audienceApp: launcherAuth.audienceApp }) contextBridge.exposeInMainWorld('electron', electronAPI) contextBridge.exposeInMainWorld('launcherAuth', launcherAuth) contextBridge.exposeInMainWorld('lmsBootSplash', { status: (message: string) => ipcRenderer.send('lms-boot-status', message), done: () => ipcRenderer.send('lms-boot-done') }) contextBridge.exposeInMainWorld('titlebar', { minimize: () => ipcRenderer.send('titlebar-minimize'), maximize: () => ipcRenderer.send('titlebar-maximize'), close: () => ipcRenderer.send('titlebar-close') }) contextBridge.exposeInMainWorld('learningVideo', { open: (url: string, title?: string) => ipcRenderer.invoke('learning-video:open', { url, title }) }) contextBridge.exposeInMainWorld('moduleFiles', { read: (moduleName: string, workspaceRoot?: string, folderName?: string) => ipcRenderer.invoke('module-files:read', moduleName, workspaceRoot, folderName), create: (moduleName: string, path: string, kind: 'file' | 'folder', workspaceRoot?: string, folderName?: string) => ipcRenderer.invoke('module-files:create', moduleName, path, kind, workspaceRoot, folderName), write: (moduleName: string, path: string, content: string, workspaceRoot?: string, folderName?: string) => ipcRenderer.invoke('module-files:write', moduleName, path, content, workspaceRoot, folderName), copy: (moduleName: string, fromPath: string, toPath: string, mode: 'copy' | 'cut', workspaceRoot?: string, folderName?: string) => ipcRenderer.invoke('module-files:copy', moduleName, fromPath, toPath, mode, workspaceRoot, folderName), rename: (moduleName: string, fromPath: string, toPath: string, workspaceRoot?: string, folderName?: string) => ipcRenderer.invoke('module-files:rename', moduleName, fromPath, toPath, workspaceRoot, folderName), delete: (moduleName: string, path: string, workspaceRoot?: string, folderName?: string) => ipcRenderer.invoke('module-files:delete', moduleName, path, workspaceRoot, folderName) }) contextBridge.exposeInMainWorld('modules', { list: (workspaceRoot?: string, modules?: Array<{ sortOrder?: number | null; category: string; name: string; folderName: string; templateRepositoryUrl?: string | null }>) => ipcRenderer.invoke('modules:list', workspaceRoot, modules), install: (moduleName: string, workspaceRoot?: string, folderName?: string, templateRepositoryUrl?: string | null, studentRepositoryUrl?: string | null, options?: ModuleInstallOptions | null) => ipcRenderer.invoke('modules:install', moduleName, workspaceRoot, folderName, templateRepositoryUrl, studentRepositoryUrl, options), delete: (moduleName: string, workspaceRoot?: string, folderName?: string) => ipcRenderer.invoke('modules:delete', moduleName, workspaceRoot, folderName), start: (moduleName: string, workspaceRoot?: string, folderName?: string, rosParams?: string, gpuEnabled?: boolean, rosDomainId?: string, runtimeImages?: RuntimeImageArtifact[] | null) => ipcRenderer.invoke('modules:start', moduleName, workspaceRoot, folderName, rosParams, gpuEnabled, rosDomainId, runtimeImages), build: (moduleName: string, workspaceRoot?: string, folderName?: string, gpuEnabled?: boolean, rosDomainId?: string, runtimeImages?: RuntimeImageArtifact[] | null) => ipcRenderer.invoke('modules:build', moduleName, workspaceRoot, folderName, gpuEnabled, rosDomainId, runtimeImages), stop: (moduleName: string, workspaceRoot?: string, folderName?: string) => ipcRenderer.invoke('modules:stop', moduleName, workspaceRoot, folderName), stopMany: (modules: Array<{ moduleName: string; folderName?: string | null }>, workspaceRoot?: string) => ipcRenderer.invoke('modules:stop-many', modules, workspaceRoot), stopRunning: () => ipcRenderer.invoke('modules:stop-running'), restart: (moduleName: string, workspaceRoot?: string, folderName?: string, rosParams?: string, gpuEnabled?: boolean, rosDomainId?: string, runtimeImages?: RuntimeImageArtifact[] | null) => ipcRenderer.invoke('modules:restart', moduleName, workspaceRoot, folderName, rosParams, gpuEnabled, rosDomainId, runtimeImages), logs: (moduleName: string, workspaceRoot?: string, folderName?: string) => ipcRenderer.invoke('modules:logs', moduleName, workspaceRoot, folderName), updateParameters: (moduleName: string, workspaceRoot?: string, folderName?: string, rosParams?: string) => ipcRenderer.invoke('modules:update-parameters', moduleName, workspaceRoot, folderName, rosParams) }) contextBridge.exposeInMainWorld('git', { status: (moduleName: string, workspaceRoot?: string, folderName?: string, selectedFilePath?: string | null) => ipcRenderer.invoke('git:status', moduleName, workspaceRoot, folderName, selectedFilePath), submit: (moduleName: string, workspaceRoot?: string, folderName?: string, message?: string, authorName?: string, authorEmail?: string, submissionId?: string) => ipcRenderer.invoke('git:submit', moduleName, workspaceRoot, folderName, message, authorName, authorEmail, submissionId), submissions: (modules: Array<{ moduleName: string; folderName?: string | null }>, workspaceRoot?: string) => ipcRenderer.invoke('git:submissions', modules, workspaceRoot), commitDiff: (moduleName: string, workspaceRoot?: string, folderName?: string, commitHash?: string) => ipcRenderer.invoke('git:commit-diff', moduleName, workspaceRoot, folderName, commitHash) }) contextBridge.exposeInMainWorld('exampleCode', { read: () => ipcRenderer.invoke('example-code:read') }) contextBridge.exposeInMainWorld('terminal', { start: (moduleName?: string, cols?: number, rows?: number, sessionId?: string, workspaceRoot?: string, folderName?: string) => ipcRenderer.invoke('terminal-start', moduleName, cols, rows, sessionId, workspaceRoot, folderName), resize: (cols?: number, rows?: number, sessionId?: string) => ipcRenderer.invoke('terminal-resize', cols, rows, sessionId), changeDirectory: (moduleName: string, sessionId?: string, workspaceRoot?: string, folderName?: string) => ipcRenderer.invoke('terminal-change-directory', moduleName, sessionId, workspaceRoot, folderName), write: (data: string, sessionId?: string) => ipcRenderer.send('terminal-write', data, sessionId), stop: (sessionId?: string) => ipcRenderer.send('terminal-stop', sessionId), onData: (callback: (data: string) => void, sessionId = 'default') => { const listener = (_event: Electron.IpcRendererEvent, dataSessionId: string, data: string): void => { if (dataSessionId === sessionId) callback(data) } ipcRenderer.on('terminal-data', listener) return () => ipcRenderer.removeListener('terminal-data', listener) }, onSuppressData: (callback: (data: string) => void, sessionId = 'default') => { const listener = (_event: Electron.IpcRendererEvent, dataSessionId: string, data: string): void => { if (dataSessionId === sessionId) callback(data) } ipcRenderer.on('terminal-suppress-data', listener) return () => ipcRenderer.removeListener('terminal-suppress-data', listener) } }) contextBridge.exposeInMainWorld('visualization', { status: () => ipcRenderer.invoke('visualization:status'), start: (stack?: 'autoware' | 'nav2' | 'slam', workspaceRoot?: string, rosDomainId?: string, environmentProfile?: unknown) => ipcRenderer.invoke('visualization:start', stack, workspaceRoot, rosDomainId, environmentProfile), stop: (environmentProfile?: unknown) => ipcRenderer.invoke('visualization:stop', environmentProfile) }) contextBridge.exposeInMainWorld('localWorkspace', { moduleFiles: { read: (moduleName: string, workspaceRoot?: string, folderName?: string) => ipcRenderer.invoke('local-workspace:module-files:read', moduleName, workspaceRoot, folderName), create: (moduleName: string, path: string, kind: 'file' | 'folder', workspaceRoot?: string, folderName?: string) => ipcRenderer.invoke('local-workspace:module-files:create', moduleName, path, kind, workspaceRoot, folderName), write: (moduleName: string, path: string, content: string, workspaceRoot?: string, folderName?: string) => ipcRenderer.invoke('local-workspace:module-files:write', moduleName, path, content, workspaceRoot, folderName), copy: (moduleName: string, fromPath: string, toPath: string, mode: 'copy' | 'cut', workspaceRoot?: string, folderName?: string) => ipcRenderer.invoke('local-workspace:module-files:copy', moduleName, fromPath, toPath, mode, workspaceRoot, folderName), rename: (moduleName: string, fromPath: string, toPath: string, workspaceRoot?: string, folderName?: string) => ipcRenderer.invoke('local-workspace:module-files:rename', moduleName, fromPath, toPath, workspaceRoot, folderName), delete: (moduleName: string, path: string, workspaceRoot?: string, folderName?: string) => ipcRenderer.invoke('local-workspace:module-files:delete', moduleName, path, workspaceRoot, folderName) }, modules: { list: (workspaceRoot?: string, modules?: Array<{ sortOrder?: number | null; category: string; name: string; folderName: string; templateRepositoryUrl?: string | null }>) => ipcRenderer.invoke('local-workspace:modules:list', workspaceRoot, modules), install: (moduleName: string, workspaceRoot?: string, folderName?: string, templateRepositoryUrl?: string | null, studentRepositoryUrl?: string | null, options?: ModuleInstallOptions | null) => ipcRenderer.invoke('local-workspace:modules:install', moduleName, workspaceRoot, folderName, templateRepositoryUrl, studentRepositoryUrl, options), delete: (moduleName: string, workspaceRoot?: string, folderName?: string) => ipcRenderer.invoke('local-workspace:modules:delete', moduleName, workspaceRoot, folderName), start: (moduleName: string, workspaceRoot?: string, folderName?: string, rosParams?: string, gpuEnabled?: boolean, rosDomainId?: string, runtimeImages?: RuntimeImageArtifact[] | null) => ipcRenderer.invoke('local-workspace:modules:start', moduleName, workspaceRoot, folderName, rosParams, gpuEnabled, rosDomainId, runtimeImages), build: (moduleName: string, workspaceRoot?: string, folderName?: string, gpuEnabled?: boolean, rosDomainId?: string, runtimeImages?: RuntimeImageArtifact[] | null) => ipcRenderer.invoke('local-workspace:modules:build', moduleName, workspaceRoot, folderName, gpuEnabled, rosDomainId, runtimeImages), stop: (moduleName: string, workspaceRoot?: string, folderName?: string) => ipcRenderer.invoke('local-workspace:modules:stop', moduleName, workspaceRoot, folderName), stopMany: (modules: Array<{ moduleName: string; folderName?: string | null }>, workspaceRoot?: string) => ipcRenderer.invoke('local-workspace:modules:stop-many', modules, workspaceRoot), stopRunning: () => ipcRenderer.invoke('local-workspace:modules:stop-running'), restart: (moduleName: string, workspaceRoot?: string, folderName?: string, rosParams?: string, gpuEnabled?: boolean, rosDomainId?: string, runtimeImages?: RuntimeImageArtifact[] | null) => ipcRenderer.invoke('local-workspace:modules:restart', moduleName, workspaceRoot, folderName, rosParams, gpuEnabled, rosDomainId, runtimeImages), logs: (moduleName: string, workspaceRoot?: string, folderName?: string) => ipcRenderer.invoke('local-workspace:modules:logs', moduleName, workspaceRoot, folderName), updateParameters: (moduleName: string, workspaceRoot?: string, folderName?: string, rosParams?: string) => ipcRenderer.invoke('local-workspace:modules:update-parameters', moduleName, workspaceRoot, folderName, rosParams) }, visualization: { status: () => ipcRenderer.invoke('local-workspace:visualization:status'), start: (stack?: 'autoware' | 'nav2' | 'slam', workspaceRoot?: string, rosDomainId?: string, environmentProfile?: unknown) => ipcRenderer.invoke('local-workspace:visualization:start', stack, workspaceRoot, rosDomainId, environmentProfile), stop: (environmentProfile?: unknown) => ipcRenderer.invoke('local-workspace:visualization:stop', environmentProfile) }, git: { status: (moduleName: string, workspaceRoot?: string, folderName?: string, selectedFilePath?: string | null) => ipcRenderer.invoke('local-workspace:git:status', moduleName, workspaceRoot, folderName, selectedFilePath), submit: (moduleName: string, workspaceRoot?: string, folderName?: string, message?: string, authorName?: string, authorEmail?: string, submissionId?: string) => ipcRenderer.invoke('local-workspace:git:submit', moduleName, workspaceRoot, folderName, message, authorName, authorEmail, submissionId), restoreFile: (moduleName: string, workspaceRoot?: string, folderName?: string, filePath?: string, status?: string) => ipcRenderer.invoke('local-workspace:git:restore-file', moduleName, workspaceRoot, folderName, filePath, status), submissions: (modules: Array<{ moduleName: string; folderName?: string | null }>, workspaceRoot?: string) => ipcRenderer.invoke('local-workspace:git:submissions', modules, workspaceRoot), commitDiff: (moduleName: string, workspaceRoot?: string, folderName?: string, commitHash?: string) => ipcRenderer.invoke('local-workspace:git:commit-diff', moduleName, workspaceRoot, folderName, commitHash) }, terminal: { start: (moduleName?: string, cols?: number, rows?: number, sessionId?: string, workspaceRoot?: string, folderName?: string) => ipcRenderer.invoke('local-workspace:terminal:start', moduleName, cols, rows, sessionId, workspaceRoot, folderName), resize: (cols?: number, rows?: number, sessionId?: string) => ipcRenderer.invoke('local-workspace:terminal:resize', cols, rows, sessionId), changeDirectory: (moduleName: string, sessionId?: string, workspaceRoot?: string, folderName?: string) => ipcRenderer.invoke('local-workspace:terminal:change-directory', moduleName, sessionId, workspaceRoot, folderName), write: (data: string, sessionId?: string) => ipcRenderer.send('local-workspace:terminal:write', data, sessionId), stop: (sessionId?: string) => ipcRenderer.send('local-workspace:terminal:stop', sessionId), onData: (callback: (data: string) => void, sessionId = 'default') => { const listener = (_event: Electron.IpcRendererEvent, dataSessionId: string, data: string): void => { if (dataSessionId === sessionId) callback(data) } ipcRenderer.on('terminal-data', listener) return () => ipcRenderer.removeListener('terminal-data', listener) }, onSuppressData: (callback: (data: string) => void, sessionId = 'default') => { const listener = (_event: Electron.IpcRendererEvent, dataSessionId: string, data: string): void => { if (dataSessionId === sessionId) callback(data) } ipcRenderer.on('terminal-suppress-data', listener) return () => ipcRenderer.removeListener('terminal-suppress-data', listener) } } }) contextBridge.exposeInMainWorld('remoteWorkspace', { moduleFiles: { read: (target: RemoteWorkspaceTarget, moduleName: string, folderName?: string) => ipcRenderer.invoke('remote-workspace:module-files:read', target, moduleName, folderName), create: (target: RemoteWorkspaceTarget, moduleName: string, path: string, kind: 'file' | 'folder', folderName?: string) => ipcRenderer.invoke('remote-workspace:module-files:create', target, moduleName, path, kind, folderName), write: (target: RemoteWorkspaceTarget, moduleName: string, path: string, content: string, folderName?: string) => ipcRenderer.invoke('remote-workspace:module-files:write', target, moduleName, path, content, folderName), copy: (target: RemoteWorkspaceTarget, moduleName: string, fromPath: string, toPath: string, mode: 'copy' | 'cut', folderName?: string) => ipcRenderer.invoke('remote-workspace:module-files:copy', target, moduleName, fromPath, toPath, mode, folderName), rename: (target: RemoteWorkspaceTarget, moduleName: string, fromPath: string, toPath: string, folderName?: string) => ipcRenderer.invoke('remote-workspace:module-files:rename', target, moduleName, fromPath, toPath, folderName), delete: (target: RemoteWorkspaceTarget, moduleName: string, path: string, folderName?: string) => ipcRenderer.invoke('remote-workspace:module-files:delete', target, moduleName, path, folderName) }, modules: { list: (target: RemoteWorkspaceTarget, modules?: Array<{ sortOrder?: number | null; category: string; name: string; folderName: string; templateRepositoryUrl?: string | null }>) => ipcRenderer.invoke('remote-workspace:modules:list', target, modules), install: (target: RemoteWorkspaceTarget, moduleName: string, folderName?: string, templateRepositoryUrl?: string | null, studentRepositoryUrl?: string | null, options?: ModuleInstallOptions | null) => ipcRenderer.invoke('remote-workspace:modules:install', target, moduleName, folderName, templateRepositoryUrl, studentRepositoryUrl, options), start: (target: RemoteWorkspaceTarget, moduleName: string, folderName?: string, rosParams?: string, gpuEnabled?: boolean, rosDomainId?: string, runtimeImages?: RuntimeImageArtifact[] | null) => ipcRenderer.invoke('remote-workspace:modules:start', target, moduleName, folderName, rosParams, gpuEnabled, rosDomainId, runtimeImages), build: (target: RemoteWorkspaceTarget, moduleName: string, folderName?: string, gpuEnabled?: boolean, rosDomainId?: string, runtimeImages?: RuntimeImageArtifact[] | null) => ipcRenderer.invoke('remote-workspace:modules:build', target, moduleName, folderName, gpuEnabled, rosDomainId, runtimeImages), stop: (target: RemoteWorkspaceTarget, moduleName: string, folderName?: string) => ipcRenderer.invoke('remote-workspace:modules:stop', target, moduleName, folderName), stopMany: (target: RemoteWorkspaceTarget, modules: Array<{ moduleName: string; folderName?: string | null }>) => ipcRenderer.invoke('remote-workspace:modules:stop-many', target, modules), resetSession: (target: RemoteWorkspaceTarget, modules: Array<{ moduleName: string; folderName?: string | null }>) => ipcRenderer.invoke('remote-workspace:modules:reset-session', target, modules), restart: (target: RemoteWorkspaceTarget, moduleName: string, folderName?: string, rosParams?: string, gpuEnabled?: boolean, rosDomainId?: string, runtimeImages?: RuntimeImageArtifact[] | null) => ipcRenderer.invoke('remote-workspace:modules:restart', target, moduleName, folderName, rosParams, gpuEnabled, rosDomainId, runtimeImages), logs: (target: RemoteWorkspaceTarget, moduleName: string, folderName?: string) => ipcRenderer.invoke('remote-workspace:modules:logs', target, moduleName, folderName) }, git: { status: (target: RemoteWorkspaceTarget, moduleName: string, folderName?: string, selectedFilePath?: string | null) => ipcRenderer.invoke('remote-workspace:git:status', target, moduleName, folderName, selectedFilePath), submit: (target: RemoteWorkspaceTarget, moduleName: string, folderName?: string, message?: string, authorName?: string, authorEmail?: string, submissionId?: string) => ipcRenderer.invoke('remote-workspace:git:submit', target, moduleName, folderName, message, authorName, authorEmail, submissionId), restoreFile: (target: RemoteWorkspaceTarget, moduleName: string, folderName?: string, filePath?: string, status?: string) => ipcRenderer.invoke('remote-workspace:git:restore-file', target, moduleName, folderName, filePath, status), submissions: (target: RemoteWorkspaceTarget, modules: Array<{ moduleName: string; folderName?: string | null }>) => ipcRenderer.invoke('remote-workspace:git:submissions', target, modules), commitDiff: (target: RemoteWorkspaceTarget, moduleName: string, folderName?: string, commitHash?: string) => ipcRenderer.invoke('remote-workspace:git:commit-diff', target, moduleName, folderName, commitHash) }, terminal: { start: (target: RemoteWorkspaceTarget, moduleName?: string, cols?: number, rows?: number, sessionId?: string, folderName?: string) => ipcRenderer.invoke('remote-workspace:terminal:start', target, moduleName, cols, rows, sessionId, folderName), resize: (target: RemoteWorkspaceTarget, cols?: number, rows?: number, sessionId?: string) => ipcRenderer.invoke('remote-workspace:terminal:resize', target, cols, rows, sessionId), changeDirectory: (target: RemoteWorkspaceTarget, moduleName: string, sessionId?: string, folderName?: string) => ipcRenderer.invoke('remote-workspace:terminal:change-directory', target, moduleName, sessionId, folderName), write: (data: string, sessionId?: string) => ipcRenderer.send('remote-workspace:terminal:write', data, sessionId), stop: (sessionId?: string) => ipcRenderer.send('remote-workspace:terminal:stop', sessionId), onData: (callback: (data: string) => void, sessionId = 'default') => { const listener = (_event: Electron.IpcRendererEvent, incomingSessionId: string, data: string) => { if (incomingSessionId === sessionId) callback(data) } ipcRenderer.on('terminal-data', listener) return () => ipcRenderer.removeListener('terminal-data', listener) }, onSuppressData: (callback: (data: string) => void, sessionId = 'default') => { const listener = (_event: Electron.IpcRendererEvent, incomingSessionId: string, data: string) => { if (incomingSessionId === sessionId) callback(data) } ipcRenderer.on('terminal-suppress-data', listener) return () => ipcRenderer.removeListener('terminal-suppress-data', listener) } } })