feat(native): implement Phase 5 AppleScript handlers and remote control

- Implemented specialized PowerPoint/Keynote handlers with auto-focus and slideshow start.
- Added native:control-presentation for remote navigation (next/prev/stop).
- Added native:list-tools handler for self-documenting the bridge API.
- Added a comprehensive project README.md.
This commit is contained in:
Scott Idem
2026-01-26 16:18:00 -05:00
parent 7784f7f2a3
commit 3d7aa1ab92
6 changed files with 244 additions and 22 deletions

70
README.md Normal file
View File

@@ -0,0 +1,70 @@
# Aether Native Launcher (Electron)
The Aether Native Launcher is a specialized Electron-based shell for the Aether Platform. It provides a secure bridge between the SvelteKit web UI and the local operating system, enabling features restricted by browser sandboxing.
## 🚀 Overview
This application serves as the "Native Mode" runtime for Aether podiums and devices. It handles:
- **Local File Orchestration:** Managed cache for presentation files (PPTx, Keynote, PDF).
- **Automation:** Specialized AppleScript handlers for PowerPoint and Keynote.
- **Hardware Telemetry:** Direct access to CPU, RAM, and Network interface data.
- **Remote Control:** Slide navigation and application control via WebSocket intents.
## ⚙️ Configuration
The application requires a `seed_config.json` file to identify the device and connect to the Aether API.
### 1. Seed Configuration
Create/edit `resources/seed_config.json`:
```json
{
"event_device_id": "YOUR_DEVICE_ID",
"aether_api_key": "YOUR_API_KEY",
"primary_api_base_url": "https://api.yourdomain.com",
"backup_api_base_url": null
}
```
### 2. Environment Setup
```bash
npm install
npm run build
npm start
```
## 🌉 The Native Bridge (`aetherNative`)
The bridge is exposed to the renderer via `contextBridge`. It can be accessed in the web UI via `window.aetherNative`.
### Core Methods
| Method | Description |
| --- | --- |
| `list_tools()` | Returns a JSON manifest of all available native functions. |
| `launch_presentation({path, app})` | Launches a presentation with auto-focus and slideshow start. |
| `control_presentation({app, action})` | Sends `next`, `prev`, `start`, or `stop` to active decks. |
| `open_folder(path)` | Opens a local directory in the OS file explorer. |
| `get_device_info()` | Returns hardware metadata (RAM, IPs, Hostname). |
### Example Usage (UI Relay)
```typescript
import * as native from '$lib/electron/electron_relay';
// Launch a file from local cache
await native.launch_presentation({
path: '[tmp]/my_deck.pptx',
app: 'powerpoint'
});
// Navigate slides
await native.control_presentation({
app: 'powerpoint',
action: 'next'
});
```
## 🛠️ Development
- **Preload:** Logic defined in `src/preload/index.ts`.
- **Handlers:** OS-level logic in `src/main/shell_handlers.ts` and `src/main/file_handlers.ts`.
- **Types:** Shared TypeScript interfaces in `src/shared/types.ts`.

View File

@@ -8,6 +8,8 @@
"git.autofetch": true,
"markdownlint.config": {
"MD007": false,
"MD030": false,
"MD004": false,
"MD033": false
}
}

View File

@@ -90,15 +90,37 @@ export function registerFileHandlers() {
});
}
if (os.platform() === 'darwin' && ext === 'key') {
console.log(`Native: Launching Keynote via AppleScript for ${target}`);
const script = `tell application "Keynote" to open POSIX file "${target}"`;
return new Promise((resolve) => {
exec(`osascript -e "${script.replace(/"/g, '\"')}"`, (err) => {
if (err) resolve({ success: false, error: err.message });
else resolve({ success: true });
if (os.platform() === 'darwin') {
let script = '';
if (ext === 'key') {
script = `
tell application "Keynote"
activate
open (POSIX file "${target}")
delay 1
start (front document)
end tell
`;
} else if (ext === 'pptx' || ext === 'ppt') {
script = `
tell application "Microsoft PowerPoint"
activate
open (POSIX file "${target}")
delay 1
run slide show of active presentation
end tell
`;
}
if (script) {
console.log(`Native: Launching ${ext} via AppleScript for ${target}`);
return new Promise((resolve) => {
exec(`osascript -e "${script.replace(/"/g, '\\\\"')}"`, (err) => {
if (err) resolve({ success: false, error: err.message });
else resolve({ success: true });
});
});
});
}
}
}
@@ -109,4 +131,4 @@ export function registerFileHandlers() {
return { success: false, error: error.message };
}
});
}
}

View File

@@ -79,10 +79,30 @@ export function registerShellHandlers() {
}
if (os.platform() === 'darwin') {
let script = '';
if (appType === 'keynote') {
const script = `tell application "Keynote" to open POSIX file "${cleanedPath}"`;
const escapedScript = script.replace(/"/g, '\\"');
script = `
tell application "Keynote"
activate
open (POSIX file "${cleanedPath}")
delay 1
start (front document)
end tell
`;
} else if (appType === 'powerpoint') {
script = `
tell application "Microsoft PowerPoint"
activate
open (POSIX file "${cleanedPath}")
delay 1
run slide show of active presentation
end tell
`;
}
if (script) {
return new Promise((resolve) => {
const escapedScript = script.replace(/"/g, '\\"');
exec(`osascript -e "${escapedScript}"`, (err, stdout, stderr) => {
if (err) resolve({ success: false, error: err.message });
else resolve({ success: true });
@@ -94,4 +114,98 @@ export function registerShellHandlers() {
const error = await shell.openPath(cleanedPath);
return { success: !error, error };
});
ipcMain.handle('native:control-presentation', async (event, { app, action }) => {
if (os.platform() !== 'darwin') return { success: false, error: 'Presentation control is only available on macOS' };
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 { success: false, error: `Unsupported app or action: ${app}/${action}` };
return new Promise((resolve) => {
exec(`osascript -e "${script.replace(/"/g, '\\"')}"`, (error, stdout, stderr) => {
resolve({ success: !error, stdout: stdout.trim(), stderr: stderr.trim(), error: error ? error.message : null });
});
});
});
ipcMain.handle('native:list-tools', async () => {
return [
{
name: 'open_folder',
description: 'Opens a directory in the OS file explorer (Finder/Files/Explorer).',
params: { path: 'string' }
},
{
name: 'run_cmd',
description: 'Executes an asynchronous shell command with a timeout.',
params: { cmd: 'string', timeout: 'number (optional)' }
},
{
name: 'run_cmd_sync',
description: 'Executes a synchronous shell command.',
params: { cmd: 'string' }
},
{
name: 'run_osascript',
description: 'Executes a raw AppleScript string (macOS only).',
params: { script: 'string' }
},
{
name: 'kill_processes',
description: 'Forcefully terminates processes by name.',
params: { process_name_li: 'string[]' }
},
{
name: 'open_local_file_v2',
description: 'Opens a local file using the default OS handler.',
params: { filePath: 'string' }
},
{
name: 'launch_presentation',
description: 'Phase 5: Specialized launcher for PowerPoint, Keynote, and LibreOffice with auto-focus.',
params: { path: 'string', app: 'default|powerpoint|keynote' }
},
{
name: 'control_presentation',
description: 'Phase 5: Remote navigation for active slideshows.',
params: { app: 'powerpoint|keynote', action: 'next|prev|start|stop' }
},
{
name: 'check_cache',
description: 'Checks if a file exists in the local organized cache.',
params: { cache_root: 'string', hash: 'string', hash_prefix_length: 'number' }
},
{
name: 'download_to_cache',
description: 'Downloads a file from the API directly into the native cache.',
params: { url: 'string', cache_root: 'string', hash: 'string', api_key: 'string', account_id: 'string' }
},
{
name: 'launch_from_cache',
description: 'Atomic operation: Copies file from cache to temp with original name and launches via specialized handler.',
params: { cache_root: 'string', hash: 'string', temp_root: 'string', filename: 'string' }
},
{
name: 'get_device_info',
description: 'Returns hardware and OS metadata (CPUs, RAM, IP addresses, Hostname).',
params: {}
}
];
});
}

View File

@@ -17,4 +17,6 @@ contextBridge.exposeInMainWorld('aetherNative', {
download_to_cache: (args: any) => ipcRenderer.invoke('native:download-to-cache', args),
launch_from_cache: (args: any) => ipcRenderer.invoke('native:launch-from-cache', args),
launch_presentation: (args: any) => ipcRenderer.invoke('native:launch-presentation', args),
control_presentation: (args: any) => ipcRenderer.invoke('native:control-presentation', args),
list_tools: () => ipcRenderer.invoke('native:list-tools'),
});

View File

@@ -7,22 +7,34 @@ export interface SeedConfig {
}
export interface AetherNativeBridge {
getSeedConfig: () => Promise<SeedConfig | null>;
getDeviceConfig: () => Promise<any>;
getJWT: () => Promise<string | null>;
log: (message: string) => void;
get_seed_config: () => Promise<SeedConfig | null>;
get_device_config: () => Promise<any>;
get_jwt: () => Promise<string | null>;
get_device_info: () => Promise<any>;
// Shell Handlers
openFolder: (path: string) => Promise<{success: boolean, error?: string}>;
runCommand: (cmd: string) => Promise<{success: boolean, stdout: string, stderr: string, error?: string}>;
launchFile: (path: string) => Promise<{success: boolean, error?: string}>;
open_folder: (path: string) => Promise<{success: boolean, error?: string}>;
run_cmd: (args: {cmd: string, timeout?: number}) => Promise<{success: boolean, stdout: string, stderr: string, error?: string}>;
run_cmd_sync: (args: {cmd: string}) => Promise<{success: boolean, stdout: string, error?: string, stderr?: string}>;
run_osascript: (script: string) => Promise<{success: boolean, stdout: string, stderr: string, error?: string}>;
kill_processes: (args: {process_name_li: string[]}) => Promise<{success: boolean, results: any[]}>;
open_local_file_v2: (path: string) => Promise<{success: boolean, error?: string}>;
// File/Cache Handlers
checkCache: (args: {cacheRoot: string, hash: string}) => Promise<boolean>;
downloadToCache: (args: {url: string, cacheRoot: string, hash: string, apiKey: string}) => Promise<{success: boolean, path?: string, error?: string}>;
launchFromCache: (args: {cacheRoot: string, hash: string, tempRoot: string, filename: string}) => Promise<{success: boolean, error?: string}>;
check_cache: (args: {cache_root: string, hash: string, hash_prefix_length?: number}) => Promise<boolean>;
download_to_cache: (args: {url: string, cache_root: string, hash: string, api_key: string, account_id?: string, hash_prefix_length?: number}) => Promise<{success: boolean, error?: string}>;
launch_from_cache: (args: {cache_root: string, hash: string, temp_root: string, filename: string, hash_prefix_length?: number}) => Promise<{success: boolean, error?: string}>;
// Specialized Presentation Handlers (Phase 5)
launch_presentation: (args: {path: string, app?: string}) => Promise<{success: boolean, error?: string, stdout?: string, stderr?: string}>;
control_presentation: (args: {app: 'powerpoint' | 'keynote', action: 'next' | 'prev' | 'start' | 'stop'}) => Promise<{success: boolean, error?: string, stdout?: string, stderr?: string}>;
// Self-Documentation
list_tools: () => Promise<Array<{name: string, description: string, params: object}>>;
}
declare global {
interface Window {
aetherNative: AetherNativeBridge;
}
}
}