Files
OSIT-AE-App-Svelte/src/lib/electron/electron_relay.ts
2026-05-13 12:48:43 -04:00

490 lines
14 KiB
TypeScript

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();
}