import { get } from 'svelte/store'; import { ae_loc, ae_api, ae_sess, slct } from '$lib/stores/ae_stores'; /** * electron_relay.ts * TypeScript relay for Aether Native (Electron) Bridge. * Standardizes all IPC calls to snake_case. */ const native = typeof window !== 'undefined' ? (window as any).aetherNative : null; export const is_native = !!native; // 1. Core Config export async function get_device_config() { if (!native) return null; return await native.get_device_config(); } export async function get_device_info() { if (!native) return null; return await native.get_device_info(); } // 2. File & Cache Management export async function check_hash_file_cache({ cache_root, hash, hash_prefix_length = 2, verify_hash = false }: any) { if (!native) return false; return await native.check_cache({ cache_root, hash, hash_prefix_length, verify_hash }); } export async function download_to_cache({ url, cache_root, hash, api_key, account_id, hash_prefix_length = 2 }: any) { if (!native) return { success: false, error: 'Native bridge not available' }; return await native.download_to_cache({ url, cache_root, hash, api_key, account_id, hash_prefix_length }); } export async function launch_from_cache({ cache_root, hash, temp_root, filename, hash_prefix_length = 2, native_template = null }: { cache_root: string; hash: string; temp_root: string; filename: string; hash_prefix_length?: number; /** * Resolved native launch template. If provided, Electron executes this string * after the file is copied to temp. * * Two formats: * - AppleScript: multi-line string with {{path}} placeholder (macOS only) * - Shell command: prefix with "shell:" → e.g. "shell:open \"{{path}}\"" * * Configure via per-profile launch_profiles overrides in event_device.data_json or $events_loc.launcher. * If null, Electron should treat that as a missing profile error. */ native_template?: string | null; }) { if (!native) return { success: false, error: 'Native bridge not available' }; return await native.launch_from_cache({ cache_root, hash, temp_root, filename, hash_prefix_length, native_template }); } /** * Thin cache primitive — copies a cached file to the temp directory and returns * the resolved path. The caller decides what happens next. * * Preferred building block for composable launch flows on the Svelte side: * 1. copy_from_cache_to_temp(...) → { path } * 2. run_osascript(template.replace('{{path}}', path)) * OR run_cmd(`open "${path}"`) * OR whatever you need * * Use launch_from_cache when the built-in hardcoded logic is sufficient. * Use this when you want full control over what happens after the file lands in temp. */ export async function copy_from_cache_to_temp({ cache_root, hash, temp_root, filename, hash_prefix_length = 2 }: { cache_root: string; hash: string; temp_root: string; filename: string; hash_prefix_length?: number; }): Promise<{ success: boolean; path?: string; error?: string }> { if (!native) return { success: false, error: 'Native bridge not available' }; return await native.copy_from_cache_to_temp({ cache_root, hash, temp_root, filename, hash_prefix_length }); } // 3. OS Shell Commands (Phase 3) export async function open_folder(path: string) { if (!native) return { success: false, error: 'Native bridge not available' }; return await native.open_folder(path); } export async function run_cmd({ cmd, timeout = 30000, return_stdout = true }: { cmd: string; timeout?: number; return_stdout?: boolean; }) { if (!native) return { success: false, error: 'Native bridge not available' }; return await native.run_cmd({ cmd, timeout, return_stdout }); } export async function run_cmd_sync({ cmd, return_stdout = true }: { cmd: string; return_stdout?: boolean; }) { if (!native) return { success: false, error: 'Native bridge not available' }; return await native.run_cmd_sync({ cmd, return_stdout }); } /** * Stale .tmp Cleanup * Deletes in-progress download artifacts (*.tmp) older than max_age_minutes from the cache root. * Called at launcher startup to prevent cache directory bloat from interrupted downloads. * Default: 1440 minutes = 24 hours. */ export async function cleanup_tmp_files({ cache_root, max_age_minutes = 1440 }: { cache_root: string; max_age_minutes?: number; }) { if (!native) return { success: false, error: 'Native bridge not available' }; const cmd = `find "${cache_root}" -name "*.tmp" -mmin +${max_age_minutes} -type f -delete`; return await native.run_cmd({ cmd, timeout: 30000, return_stdout: false }); } /** * Executes an AppleScript string. macOS only. * * HARDENED (2026-05-11): The Electron handler now writes the script to a temp .scpt * file and runs `osascript "/path/to/file.scpt"` rather than passing it inline via * the -e flag. This means: * - Multi-line scripts work correctly * - Paths with spaces or special characters work correctly * - No shell escaping required in the script string you pass here * * The .scpt file is deleted immediately after execution. */ export async function run_osascript(script: string) { if (!native) return { success: false, error: 'Native bridge not available' }; return await native.run_osascript(script); } export async function kill_processes({ process_name_li = [] }: { process_name_li: string[]; }) { if (!native) return { success: false, error: 'Native bridge not available' }; return await native.kill_processes({ process_name_li }); } export async function open_local_file_v2(path: string) { if (!native) return { success: false, error: 'Native bridge not available' }; return await native.open_local_file_v2(path); } /** * Specialized Presentation Launcher (Phase 5) * Handles platform-specific application selection (LibreOffice on Linux, * PowerPoint/Keynote on macOS). */ export async function launch_presentation({ path, app = 'default', os = 'auto', log_lvl = 0 }: { path: string; app?: 'default' | 'powerpoint' | 'keynote' | 'libreoffice'; os?: 'auto' | 'linux' | 'darwin' | 'win32'; log_lvl?: number; }) { if (!native) return { success: false, error: 'Native bridge not available' }; // 1. Detect OS if set to auto let platform = os; if (platform === 'auto') { const info = await get_device_info(); platform = info?.platform || 'linux'; } // 2. Prefer the Native Bridge implementation (Atomic Copy-and-Launch) // This delegates to the hardened logic in electron_native.js if (native.launch_presentation) { if (log_lvl) console.log('Relay: Using native.launch_presentation'); return await native.launch_presentation({ path, app, os_platform: platform }); } // 3. Relay-side Fallback (Mock/Legacy) // Manually resolve placeholders using all available context const info = await get_device_info(); const loc = get(ae_loc); // Attempt to find home/tmp from bridge info OR local hydrated store const home = info?.home_directory || loc.home_directory || loc.native_device?.home_directory || ''; const tmp = info?.tmp_directory || loc.tmp_directory || loc.native_device?.tmp_directory || ''; if (log_lvl) console.log('Relay Debug:', { home, tmp, raw_path: path }); // CRITICAL: Resolve all instances of placeholders using global regex let cleaned_path = path; if (home) cleaned_path = cleaned_path.replace(/\[home\]/g, home); if (tmp) cleaned_path = cleaned_path.replace(/\[tmp\]/g, tmp); if (log_lvl) console.log(`Relay Fallback: Resolving ${path} -> ${cleaned_path}`); // If path still contains [home] or [tmp], it means we failed to resolve it. if (cleaned_path.includes('[home]') || cleaned_path.includes('[tmp]')) { console.error( 'Relay Error: Could not resolve path placeholders. Home or Tmp directory unknown.', { home, tmp } ); return { success: false, error: 'Could not resolve path placeholders' }; } if (platform === 'linux') { if (log_lvl) console.log( `Relay: Launching LibreOffice on Linux for path: ${cleaned_path}` ); return await run_cmd({ cmd: `libreoffice --impress "${cleaned_path}"` }); } if (platform === 'darwin') { if (app === 'keynote') { return await run_osascript( `tell application "Keynote" to open POSIX file "${cleaned_path}"` ); } return await open_local_file_v2(cleaned_path); } return await open_local_file_v2(cleaned_path); } /** * Control Presentation (Phase 5) * Sends navigation commands to the active presentation (Next, Prev, Stop). */ export async function control_presentation({ app, action }: { app: 'powerpoint' | 'keynote'; action: 'next' | 'prev' | 'start' | 'stop'; }) { if (!native) return { success: false, error: 'Native bridge not available' }; // Check if the native bridge has the direct implementation if (native.control_presentation) { return await native.control_presentation({ app, action }); } // Fallback to generic osascript if direct handler is missing let script = ''; if (app === 'powerpoint') { switch (action) { case 'next': script = 'tell application "Microsoft PowerPoint" to next slide of slide show view of active presentation'; break; case 'prev': script = 'tell application "Microsoft PowerPoint" to previous slide of slide show view of active presentation'; break; case 'start': script = 'tell application "Microsoft PowerPoint" to run slide show of active presentation'; break; case 'stop': script = 'tell application "Microsoft PowerPoint" to stop slide show of active presentation'; break; } } else if (app === 'keynote') { switch (action) { case 'next': script = 'tell application "Keynote" to show next'; break; case 'prev': script = 'tell application "Keynote" to show previous'; break; case 'start': script = 'tell application "Keynote" to start (front document)'; break; case 'stop': script = 'tell application "Keynote" to stop'; break; } } if (script) { return await run_osascript(script); } return { success: false, error: `Unsupported app or action: ${app}/${action}` }; } // 4. System Management (Phase 5+) export async function set_wallpaper({ path }: { path: string }) { if (!native || !native.set_wallpaper) return { success: false, error: 'Native handler set_wallpaper not available' }; return await native.set_wallpaper({ path }); } export async function update_app(args: { source: 'url' | 'file'; url?: string; path?: string; }) { if (!native || !native.update_app) return { success: false, error: 'Native handler update_app not available' }; return await native.update_app(args); } export async function window_control({ action, value }: { action: string; value?: any; }) { if (!native || !native.window_control) return { success: false, error: 'Native handler window_control not available' }; return await native.window_control({ action, value }); } export async function manage_recording({ action, options }: { action: 'start' | 'stop' | 'status'; options?: any; }) { if (!native || !native.manage_recording) return { success: false, error: 'Native handler manage_recording not available' }; return await native.manage_recording({ action, options }); } export async function set_display_layout({ mode, configStr }: { mode: 'mirror' | 'extend'; configStr?: string; }) { if (!native || !native.set_display_layout) return { success: false, error: 'Native handler set_display_layout not available' }; return await native.set_display_layout({ mode, configStr }); } export async function power_control({ action }: { action: 'shutdown' | 'reboot' | 'sleep'; }) { if (!native || !native.power_control) return { success: false, error: 'Native handler power_control not available' }; return await native.power_control({ action }); } export async function open_external({ url, app }: { url: string; app?: 'chrome' | 'firefox' | 'default'; }) { if (!native || !native.open_external) return { success: false, error: 'Native handler open_external not available' }; return await native.open_external({ url, app }); } /** * List Tools (Self-Documentation) * Returns a JSON manifest of all available native bridge functions. */ export async function list_tools() { if (!native || !native.list_tools) return []; return await native.list_tools(); }