import { execFile as execFileCallback, spawn } from 'node:child_process' import { mkdtemp, readFile, readdir, rm, stat } from 'node:fs/promises' import { homedir, tmpdir } from 'node:os' import { extname, join, posix as posixPath, resolve, win32 as winPath } from 'node:path' import { promisify } from 'node:util' import { ApiError } from '../middleware/errorHandler.js' const execFile = promisify(execFileCallback) const DEFAULT_TTL_MS = 30_000 export type OpenTargetPlatform = NodeJS.Platform export type OpenTargetKind = 'ide' | 'file_manager' export type OpenTarget = { id: string kind: OpenTargetKind label: string icon: string iconUrl?: string platform: OpenTargetPlatform } export type OpenTargetList = { platform: OpenTargetPlatform targets: OpenTarget[] primaryTargetId: string | null cachedAt: number ttlMs: number } export type OpenTargetLaunchResult = { code: number stdout: string stderr: string } export type OpenTargetIconResult = { contentType: 'image/png' data: Uint8Array } type Runtime = { platform: OpenTargetPlatform ttlMs: number now: () => number commandExists: (command: string) => Promise resolveCommand: (command: string) => Promise pathExists: (targetPath: string) => Promise launch: (command: string, args: string[]) => Promise readDirNames: (targetPath: string) => Promise readTextFile: (targetPath: string) => Promise readPlistValue: (plistPath: string, key: string) => Promise convertIconToPng: (iconPath: string, size: number) => Promise } type LaunchPlan = { command: string args: string[] } type TargetDefinition = { id: string kind: OpenTargetKind label: string icon: string platforms: OpenTargetPlatform[] commands?: Partial> appPaths?: Partial> iconPaths?: Partial> windowsExecutableNames?: string[] fallback?: boolean } const TARGET_DEFINITIONS: TargetDefinition[] = [ { id: 'vscode', kind: 'ide', label: 'VS Code', icon: 'vscode', platforms: ['darwin', 'win32', 'linux'], commands: { darwin: ['code'], win32: ['code.cmd', 'code.exe'], linux: ['code'], }, windowsExecutableNames: ['Code.exe'], appPaths: { darwin: [ '/Applications/Visual Studio Code.app', posixPath.join(homedir(), 'Applications', 'Visual Studio Code.app'), ], }, }, { id: 'cursor', kind: 'ide', label: 'Cursor', icon: 'cursor', platforms: ['darwin', 'win32', 'linux'], commands: { darwin: ['cursor'], win32: ['cursor.cmd', 'cursor.exe'], linux: ['cursor'], }, windowsExecutableNames: ['Cursor.exe'], appPaths: { darwin: ['/Applications/Cursor.app', posixPath.join(homedir(), 'Applications', 'Cursor.app')], }, }, { id: 'sublime', kind: 'ide', label: 'Sublime Text', icon: 'sublime', platforms: ['darwin', 'win32', 'linux'], commands: { darwin: ['subl'], win32: ['subl.exe', 'subl'], linux: ['subl'], }, windowsExecutableNames: ['sublime_text.exe', 'subl.exe'], appPaths: { darwin: ['/Applications/Sublime Text.app', posixPath.join(homedir(), 'Applications', 'Sublime Text.app')], }, }, { id: 'antigravity', kind: 'ide', label: 'Antigravity', icon: 'antigravity', platforms: ['darwin'], commands: { darwin: ['antigravity'], }, appPaths: { darwin: ['/Applications/Antigravity.app', posixPath.join(homedir(), 'Applications', 'Antigravity.app')], }, }, { id: 'goland', kind: 'ide', label: 'GoLand', icon: 'goland', platforms: ['darwin', 'win32', 'linux'], commands: { darwin: ['goland'], win32: ['goland64.exe', 'goland.cmd'], linux: ['goland'], }, windowsExecutableNames: ['goland64.exe', 'goland.exe'], appPaths: { darwin: ['/Applications/GoLand.app', posixPath.join(homedir(), 'Applications', 'GoLand.app')], }, }, { id: 'pycharm', kind: 'ide', label: 'PyCharm', icon: 'pycharm', platforms: ['darwin', 'win32', 'linux'], commands: { darwin: ['pycharm'], win32: ['pycharm64.exe', 'pycharm.cmd'], linux: ['pycharm'], }, windowsExecutableNames: ['pycharm64.exe', 'pycharm.exe'], appPaths: { darwin: ['/Applications/PyCharm.app', posixPath.join(homedir(), 'Applications', 'PyCharm.app')], }, }, { id: 'finder', kind: 'file_manager', label: 'Finder', icon: 'finder', platforms: ['darwin'], iconPaths: { darwin: ['/System/Library/CoreServices/Finder.app/Contents/Resources/Finder.icns'], }, fallback: true, }, { id: 'explorer', kind: 'file_manager', label: 'Explorer', icon: 'folder', platforms: ['win32'], fallback: true, }, { id: 'file-manager', kind: 'file_manager', label: 'File Manager', icon: 'folder', platforms: ['linux'], fallback: true, }, ] const LINUX_APPLICATION_DIRS = [ '/usr/share/applications', '/usr/local/share/applications', posixPath.join(homedir(), '.local', 'share', 'applications'), '/var/lib/flatpak/exports/share/applications', posixPath.join(homedir(), '.local', 'share', 'flatpak', 'exports', 'share', 'applications'), ] const LINUX_ICON_ROOTS = [ posixPath.join(homedir(), '.local', 'share', 'icons'), '/usr/local/share/icons', '/usr/share/icons', ] const LINUX_ICON_THEME_SUBDIRS = [ 'scalable/apps', '512x512/apps', '256x256/apps', '128x128/apps', '64x64/apps', '48x48/apps', '32x32/apps', 'apps/scalable', 'apps/64', 'apps/48', 'apps/32', ] function openTargetError(statusCode: number, message: string, code: string): ApiError { return new ApiError(statusCode, message, code) } async function defaultResolveCommand(command: string): Promise { const probe = process.platform === 'win32' ? 'where' : 'which' try { const { stdout } = await execFile(probe, [command], { timeout: 3_000, windowsHide: true, }) const firstPath = String(stdout ?? '') .split(/\r?\n/) .map((line) => line.trim()) .find(Boolean) return firstPath ?? null } catch { return null } } async function defaultCommandExists(command: string): Promise { return (await defaultResolveCommand(command)) !== null } async function defaultPathExists(targetPath: string): Promise { try { const entry = await stat(targetPath) return entry.isFile() || entry.isDirectory() } catch { return false } } async function defaultLaunch(command: string, args: string[]): Promise { return await new Promise((resolveLaunch) => { let settled = false const settle = (result: OpenTargetLaunchResult) => { if (settled) return settled = true resolveLaunch(result) } try { const child = spawn(command, args, { detached: true, stdio: 'ignore', windowsHide: true, }) child.once('error', (error) => { settle({ code: 1, stdout: '', stderr: error.message, }) }) child.once('spawn', () => { child.unref() settle({ code: 0, stdout: '', stderr: '', }) }) } catch (error) { const err = error as { message?: string } settle({ code: 1, stdout: '', stderr: String(err.message ?? error), }) } }) } async function resolveWindowsApplicationPath( definition: TargetDefinition, runtime: Runtime, ): Promise { for (const appPath of definition.appPaths?.win32 ?? []) { if (await runtime.pathExists(appPath)) return appPath } for (const command of definition.commands?.win32 ?? []) { const commandPath = await runtime.resolveCommand(command) if (!commandPath) continue const executablePath = await resolveWindowsExecutablePath(commandPath, definition, runtime) if (executablePath) return executablePath } return null } async function resolveWindowsExecutablePath( commandPath: string, definition: TargetDefinition, runtime: Runtime, ): Promise { const extension = winPath.extname(commandPath).toLowerCase() if (extension === '.exe' && await runtime.pathExists(commandPath)) { return commandPath } if (extension !== '.cmd' && extension !== '.bat') { return null } const executableNames = definition.windowsExecutableNames ?? definition.commands?.win32?.filter((command) => winPath.extname(command).toLowerCase() === '.exe') ?? [] let currentDir = winPath.dirname(commandPath) for (let depth = 0; depth < 5; depth += 1) { for (const executableName of executableNames) { const candidate = winPath.join(currentDir, executableName) if (await runtime.pathExists(candidate)) return candidate } const nextDir = winPath.dirname(currentDir) if (!nextDir || nextDir === currentDir) break currentDir = nextDir } return null } async function defaultReadDirNames(targetPath: string): Promise { try { return await readdir(targetPath) } catch { return [] } } async function defaultReadTextFile(targetPath: string): Promise { try { return await readFile(targetPath, 'utf8') } catch { return null } } async function defaultReadPlistValue(plistPath: string, key: string): Promise { try { const { stdout } = await execFile('/usr/bin/plutil', [ '-extract', key, 'raw', '-o', '-', plistPath, ], { timeout: 3_000, windowsHide: true, }) const value = String(stdout ?? '').trim() return value || null } catch { return null } } async function defaultConvertIconToPng(iconPath: string, size: number): Promise { const extension = extname(iconPath).toLowerCase() if (extension === '.png') { return await readFile(iconPath) } const tmpDir = await mkdtemp(join(tmpdir(), 'cc-haha-open-target-icon-')) const outputPath = join(tmpDir, 'icon.png') try { if (process.platform === 'win32') { await convertWindowsIconToPng(iconPath, outputPath) } else if (extension === '.svg' || extension === '.xpm') { await convertLinuxThemeIconToPng(iconPath, outputPath, size) } else { await execFile('/usr/bin/sips', [ '-z', String(size), String(size), '-s', 'format', 'png', iconPath, '--out', outputPath, ], { timeout: 5_000, windowsHide: true, }) } return await readFile(outputPath) } finally { await rm(tmpDir, { recursive: true, force: true }) } } async function convertWindowsIconToPng(iconPath: string, outputPath: string): Promise { const script = ` Add-Type -AssemblyName System.Drawing $source = $env:CC_HAHA_ICON_SOURCE $output = $env:CC_HAHA_ICON_OUTPUT $icon = [System.Drawing.Icon]::ExtractAssociatedIcon($source) if ($null -eq $icon) { exit 2 } $bitmap = $icon.ToBitmap() $bitmap.Save($output, [System.Drawing.Imaging.ImageFormat]::Png) $bitmap.Dispose() $icon.Dispose() ` await execFile('powershell.exe', [ '-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-Command', script, ], { env: { ...process.env, CC_HAHA_ICON_SOURCE: iconPath, CC_HAHA_ICON_OUTPUT: outputPath, }, timeout: 5_000, windowsHide: true, }) } async function convertLinuxThemeIconToPng( iconPath: string, outputPath: string, size: number, ): Promise { const attempts: Array<{ command: string; args: string[] }> = [ { command: 'rsvg-convert', args: ['-w', String(size), '-h', String(size), '-o', outputPath, iconPath], }, { command: 'gdk-pixbuf-thumbnailer', args: ['-s', String(size), iconPath, outputPath], }, { command: 'magick', args: [iconPath, '-resize', `${size}x${size}`, outputPath], }, { command: 'convert', args: [iconPath, '-resize', `${size}x${size}`, outputPath], }, ] let lastError: unknown for (const attempt of attempts) { try { await execFile(attempt.command, attempt.args, { timeout: 5_000, windowsHide: true, }) return } catch (error) { lastError = error } } throw lastError instanceof Error ? lastError : new Error(`Unable to rasterize Linux icon: ${iconPath}`) } function buildOpenTarget(definition: TargetDefinition, platform: OpenTargetPlatform): OpenTarget { return { id: definition.id, kind: definition.kind, label: definition.label, icon: definition.icon, iconUrl: `/api/open-targets/icons/${encodeURIComponent(definition.id)}`, platform, } } function isSupportedOnPlatform(definition: TargetDefinition, platform: OpenTargetPlatform): boolean { return definition.platforms.includes(platform) } async function isDetected(definition: TargetDefinition, runtime: Runtime): Promise { if (!isSupportedOnPlatform(definition, runtime.platform)) { return false } if (definition.fallback) { if (runtime.platform === 'linux') { return runtime.commandExists('xdg-open') } return true } if (runtime.platform === 'darwin' && definition.appPaths?.darwin?.length) { for (const appPath of definition.appPaths.darwin) { if (await runtime.pathExists(appPath)) { return true } } return false } if (runtime.platform === 'win32') { return (await resolveWindowsApplicationPath(definition, runtime)) !== null } for (const appPath of definition.appPaths?.[runtime.platform] ?? []) { if (await runtime.pathExists(appPath)) { return true } } for (const command of definition.commands?.[runtime.platform] ?? []) { if (await runtime.commandExists(command)) { return true } } return false } async function resolveLaunchPlan( definition: TargetDefinition, runtime: Runtime, targetPath: string, ): Promise { if (!isSupportedOnPlatform(definition, runtime.platform)) { return null } if (definition.fallback) { switch (runtime.platform) { case 'darwin': return { command: 'open', args: [targetPath] } case 'win32': return { command: 'cmd.exe', args: ['/d', '/c', 'start', '', targetPath] } case 'linux': return { command: 'xdg-open', args: [targetPath] } default: return null } } if (runtime.platform === 'darwin') { for (const appPath of definition.appPaths?.darwin ?? []) { if (await runtime.pathExists(appPath)) { return { command: 'open', args: ['-a', appPath, targetPath] } } } } if (runtime.platform === 'win32') { const applicationPath = await resolveWindowsApplicationPath(definition, runtime) return applicationPath ? { command: applicationPath, args: [targetPath] } : null } for (const command of definition.commands?.[runtime.platform] ?? []) { if (await runtime.commandExists(command)) { return { command, args: [targetPath] } } } return null } async function validateDirectory(targetPath: string): Promise { const resolvedPath = resolve(targetPath) let entry try { entry = await stat(resolvedPath) } catch { throw openTargetError( 400, `Directory does not exist: ${resolvedPath}`, 'OPEN_TARGET_PATH_MISSING', ) } if (!entry.isDirectory()) { throw openTargetError( 400, `Path is not a directory: ${resolvedPath}`, 'OPEN_TARGET_PATH_NOT_DIRECTORY', ) } return resolvedPath } function normalizeIconFileName(iconFile: string): string { const trimmed = iconFile.trim() if (!trimmed) return trimmed return extname(trimmed) ? trimmed : `${trimmed}.icns` } async function findDarwinBundleIconPath( appPath: string, definition: TargetDefinition, runtime: Runtime, ): Promise { const resourcesPath = posixPath.join(appPath, 'Contents', 'Resources') const plistPath = posixPath.join(appPath, 'Contents', 'Info.plist') const plistIcon = await runtime.readPlistValue(plistPath, 'CFBundleIconFile') const candidates = [ plistIcon ? normalizeIconFileName(plistIcon) : null, `${definition.label}.icns`, `${definition.icon}.icns`, ].filter((value): value is string => Boolean(value)) for (const fileName of candidates) { const iconPath = posixPath.join(resourcesPath, fileName) if (await runtime.pathExists(iconPath)) return iconPath } const iconFiles = await runtime.readDirNames(resourcesPath) const firstIcon = iconFiles .filter((fileName) => fileName.endsWith('.icns')) .find((fileName) => !/document/i.test(fileName)) ?? null if (!firstIcon) return null const fallbackPath = posixPath.join(resourcesPath, firstIcon) return await runtime.pathExists(fallbackPath) ? fallbackPath : null } async function resolveDarwinIconPath( definition: TargetDefinition, runtime: Runtime, ): Promise { for (const iconPath of definition.iconPaths?.darwin ?? []) { if (await runtime.pathExists(iconPath)) return iconPath } for (const appPath of definition.appPaths?.darwin ?? []) { if (!(await runtime.pathExists(appPath))) continue const iconPath = await findDarwinBundleIconPath(appPath, definition, runtime) if (iconPath) return iconPath } return null } async function resolveWindowsIconPath( definition: TargetDefinition, runtime: Runtime, ): Promise { if (definition.fallback && definition.id === 'explorer') { const explorerPath = await runtime.resolveCommand('explorer.exe') if (explorerPath) return explorerPath } for (const iconPath of definition.iconPaths?.win32 ?? []) { if (await runtime.pathExists(iconPath)) return iconPath } const applicationPath = await resolveWindowsApplicationPath(definition, runtime) if (applicationPath) return applicationPath return null } type LinuxDesktopEntry = { filePath: string name: string | null exec: string | null icon: string | null } async function resolveLinuxIconPath( definition: TargetDefinition, runtime: Runtime, ): Promise { for (const iconPath of definition.iconPaths?.linux ?? []) { if (await runtime.pathExists(iconPath)) return iconPath } const desktopEntries = definition.fallback ? [] : await findLinuxDesktopEntries(definition, runtime) for (const desktopEntry of desktopEntries) { if (!desktopEntry.icon) continue const iconPath = await resolveLinuxIconName(desktopEntry.icon, runtime) if (iconPath) return iconPath } if (definition.fallback && definition.kind === 'file_manager') { return await resolveLinuxIconName('folder', runtime) ?? await resolveLinuxIconName('system-file-manager', runtime) ?? await resolveLinuxIconName('org.gnome.Nautilus', runtime) } return null } async function findLinuxDesktopEntries( definition: TargetDefinition, runtime: Runtime, ): Promise { const matches: LinuxDesktopEntry[] = [] for (const directory of LINUX_APPLICATION_DIRS) { const fileNames = await runtime.readDirNames(directory) for (const fileName of fileNames) { if (!fileName.endsWith('.desktop')) continue const filePath = posixPath.join(directory, fileName) const text = await runtime.readTextFile(filePath) if (!text) continue const entry = parseLinuxDesktopEntry(filePath, text) if (entry && linuxDesktopEntryMatchesDefinition(entry, fileName, definition)) { matches.push(entry) } } } return matches } function parseLinuxDesktopEntry(filePath: string, text: string): LinuxDesktopEntry | null { let inDesktopEntry = false const values = new Map() for (const rawLine of text.split(/\r?\n/)) { const line = rawLine.trim() if (!line || line.startsWith('#')) continue if (line.startsWith('[') && line.endsWith(']')) { inDesktopEntry = line === '[Desktop Entry]' continue } if (!inDesktopEntry) continue const equalsIndex = line.indexOf('=') if (equalsIndex <= 0) continue const rawKey = line.slice(0, equalsIndex).trim() const key = rawKey.replace(/\[.*\]$/, '') if (key !== 'Name' && key !== 'Exec' && key !== 'Icon') continue values.set(key, line.slice(equalsIndex + 1).trim()) } if (!values.has('Exec') && !values.has('Icon')) return null return { filePath, name: values.get('Name') ?? null, exec: values.get('Exec') ?? null, icon: values.get('Icon') ?? null, } } function linuxDesktopEntryMatchesDefinition( entry: LinuxDesktopEntry, fileName: string, definition: TargetDefinition, ): boolean { const commandNames = new Set( (definition.commands?.linux ?? []).map((command) => posixPath.basename(command).toLowerCase()), ) const normalizedNeedles = [ definition.id, definition.icon, definition.label, ...commandNames, ].map(normalizeLinuxDesktopSearchText) const normalizedFileName = normalizeLinuxDesktopSearchText(fileName) const normalizedName = normalizeLinuxDesktopSearchText(entry.name ?? '') if (normalizedNeedles.some((needle) => needle && ( normalizedFileName.includes(needle) || normalizedName.includes(needle) ))) { return true } const execCommand = entry.exec ? extractLinuxDesktopExecCommand(entry.exec) : null return execCommand ? commandNames.has(execCommand) : false } function normalizeLinuxDesktopSearchText(value: string): string { return value.toLowerCase().replace(/[^a-z0-9]+/g, '') } function extractLinuxDesktopExecCommand(execValue: string): string | null { const tokens = execValue .replace(/%[a-zA-Z]/g, '') .match(/"([^"]+)"|'([^']+)'|(\S+)/g) ?.map((token) => token.replace(/^['"]|['"]$/g, '')) ?? [] for (const token of tokens) { if (!token || token.includes('=')) continue const command = posixPath.basename(token).toLowerCase() if (command === 'env') continue return command } return null } async function resolveLinuxIconName( iconName: string, runtime: Runtime, ): Promise { const trimmed = iconName.trim() if (!trimmed) return null if (trimmed.startsWith('/') && await runtime.pathExists(trimmed)) { return trimmed } const extension = extname(trimmed) const baseName = extension ? trimmed.slice(0, -extension.length) : trimmed const extensions = extension ? [extension] : ['.png', '.svg', '.xpm'] const directRoots = [ '/usr/share/pixmaps', '/usr/local/share/pixmaps', posixPath.join(homedir(), '.local', 'share', 'pixmaps'), ] for (const root of directRoots) { for (const candidateExtension of extensions) { const candidate = posixPath.join(root, `${baseName}${candidateExtension}`) if (await runtime.pathExists(candidate)) return candidate } } for (const root of LINUX_ICON_ROOTS) { const themeNames = await runtime.readDirNames(root) for (const themeName of themeNames) { for (const subdir of LINUX_ICON_THEME_SUBDIRS) { for (const candidateExtension of extensions) { const candidate = posixPath.join(root, themeName, subdir, `${baseName}${candidateExtension}`) if (await runtime.pathExists(candidate)) return candidate } } } } return null } async function resolveIconPath( definition: TargetDefinition, runtime: Runtime, ): Promise { if (!isSupportedOnPlatform(definition, runtime.platform)) { return null } switch (runtime.platform) { case 'darwin': return resolveDarwinIconPath(definition, runtime) case 'win32': return resolveWindowsIconPath(definition, runtime) case 'linux': return resolveLinuxIconPath(definition, runtime) default: return null } } export function createOpenTargetService(overrides: Partial = {}) { const runtime: Runtime = { platform: overrides.platform ?? process.platform, ttlMs: overrides.ttlMs ?? DEFAULT_TTL_MS, now: overrides.now ?? Date.now, commandExists: overrides.commandExists ?? defaultCommandExists, resolveCommand: overrides.resolveCommand ?? defaultResolveCommand, pathExists: overrides.pathExists ?? defaultPathExists, launch: overrides.launch ?? defaultLaunch, readDirNames: overrides.readDirNames ?? defaultReadDirNames, readTextFile: overrides.readTextFile ?? defaultReadTextFile, readPlistValue: overrides.readPlistValue ?? defaultReadPlistValue, convertIconToPng: overrides.convertIconToPng ?? defaultConvertIconToPng, } let cache: OpenTargetList | null = null const iconCache = new Map() async function listTargets(forceRefresh = false): Promise { if (!forceRefresh && cache && runtime.now() - cache.cachedAt < runtime.ttlMs) { return cache } const targets: OpenTarget[] = [] for (const definition of TARGET_DEFINITIONS) { if (await isDetected(definition, runtime)) { targets.push(buildOpenTarget(definition, runtime.platform)) } } cache = { platform: runtime.platform, targets, primaryTargetId: targets[0]?.id ?? null, cachedAt: runtime.now(), ttlMs: runtime.ttlMs, } return cache } async function openTarget(input: { targetId: string; path: string }) { const definition = TARGET_DEFINITIONS.find((candidate) => candidate.id === input.targetId) if (!definition) { throw openTargetError( 400, `Unknown open target: ${input.targetId}`, 'OPEN_TARGET_UNKNOWN', ) } const targets = await listTargets() const target = targets.targets.find((candidate) => candidate.id === input.targetId) if (!target) { throw openTargetError( 400, `Open target is not available on ${runtime.platform}: ${input.targetId}`, 'OPEN_TARGET_UNAVAILABLE', ) } const resolvedPath = await validateDirectory(input.path) const launchPlan = await resolveLaunchPlan(definition, runtime, resolvedPath) if (!launchPlan) { throw openTargetError( 400, `Unable to launch open target: ${input.targetId}`, 'OPEN_TARGET_UNAVAILABLE', ) } const launchResult = await runtime.launch(launchPlan.command, launchPlan.args) if (launchResult.code !== 0) { throw openTargetError( 500, `Failed to launch open target: ${input.targetId}`, 'OPEN_TARGET_LAUNCH_FAILED', ) } return { ok: true as const, targetId: target.id, path: resolvedPath, } } async function getTargetIcon(targetId: string, size = 64): Promise { const definition = TARGET_DEFINITIONS.find((candidate) => candidate.id === targetId) if (!definition) { throw openTargetError(404, `Unknown open target icon: ${targetId}`, 'OPEN_TARGET_ICON_UNKNOWN') } const normalizedSize = Number.isFinite(size) ? Math.min(256, Math.max(16, Math.round(size))) : 64 const cacheKey = `${runtime.platform}:${targetId}:${normalizedSize}` const cachedIcon = iconCache.get(cacheKey) if (cachedIcon) return cachedIcon const targets = await listTargets() if (!targets.targets.some((target) => target.id === targetId)) { throw openTargetError( 404, `Open target icon is not available on ${runtime.platform}: ${targetId}`, 'OPEN_TARGET_ICON_UNAVAILABLE', ) } const iconPath = await resolveIconPath(definition, runtime) if (!iconPath) { throw openTargetError( 404, `Open target icon is not available on ${runtime.platform}: ${targetId}`, 'OPEN_TARGET_ICON_UNAVAILABLE', ) } const icon = { contentType: 'image/png' as const, data: await runtime.convertIconToPng(iconPath, normalizedSize), } iconCache.set(cacheKey, icon) return icon } return { listTargets, openTarget, getTargetIcon, } } export const openTargetService = createOpenTargetService()