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.
🖥️ Onsite Deployment
Current hardware: MacBook Air 2018 — Intel x64. All current deployments use aether_launcher-darwin-x64.
Future hardware: Apple Silicon Macs use aether_launcher-darwin-arm64. Windows and Linux builds are planned.
SSH user on all laptops: speaker ready
IP pattern: 192.168.32.1XX (XX = zero-padded laptop number, e.g. 03 → .103). Find/replace this prefix for other onsite environments.
Deploy files live in deploy/:
| File | Purpose |
|---|---|
deploy/deploy.sh |
Deploy script — handles arch detection, scp, and seed.json |
deploy/devices.conf |
Laptop list: number, IP, event_device_id |
deploy/event.env |
Gitignored — per-event API key and URLs (create from example) |
deploy/event.env.example |
Template for event.env |
Step 1 — Build the app (workstation)
cd ~/OSIT_dev/aether_app_native_electron
npm run package:mac
# Produces builds/aether_launcher-darwin-x64/aether_launcher.app ← the one to deploy
Only rebuild if source code has changed. The .app bundle is identical for all Intel laptops —
only ~/seed.json differs per device.
Step 2 — Create event.env
cp deploy/event.env.example deploy/event.env
# Edit deploy/event.env — fill in AETHER_API_KEY
Create the API key in the Aether admin panel before the show (Core → Accounts or Events → Devices API key section). All laptops share one key per event. Delete it after the show.
Step 3 — Run the deploy script
# Deploy specific laptops:
./deploy/deploy.sh 01 02 03
# Deploy all laptops in devices.conf:
./deploy/deploy.sh all
# Update seed.json only (no .app copy — e.g. when rotating the API key):
./deploy/deploy.sh --seed-only all
The script auto-detects each Mac's CPU architecture, copies the correct .app build, writes
seed.json, and verifies. One SSH connection failure won't abort the batch — it logs and
continues, then reports which laptops need a retry.
Step 4 — Verify and launch
After the script completes, launch the app on each laptop and confirm it connects and shows the correct device name in the Launcher UI.
Adding SSH key to a new laptop (first time only)
ssh-copy-id "speaker ready"@192.168.32.1XX
Run once per laptop before deploying.
Manual deploy reference
The script covers the normal case. For one-off fixes or if the script isn't available:
# Detect arch
ssh "speaker ready"@192.168.32.103 "uname -m"
# x86_64 → darwin-x64 | arm64 → darwin-arm64
# Copy .app (Intel example):
scp -r builds/aether_launcher-darwin-x64/aether_launcher.app \
"speaker ready"@192.168.32.103:/Applications/aether_launcher.app
# Write seed.json:
ssh "speaker ready"@192.168.32.103 "cat > ~/seed.json" << 'EOF'
{
"event_device_id": "DEVICE_ID_FOR_THIS_LAPTOP",
"aether_api_key": "YOUR_API_KEY",
"primary_api_base_url": "https://api.oneskyit.com",
"backup_api_base_url": "https://bak-api.oneskyit.com",
"onsite_api_base_url": null
}
EOF
# Verify:
ssh "speaker ready"@192.168.32.103 "cat ~/seed.json"
event_device_id values by laptop — see the Device Reference table below.
📋 Device Reference
| Laptop | IP Address | event_device_id | Notes |
|---|---|---|---|
| 01 | 192.168.32.101 | tFLL1fLQfnk | |
| 02 | 192.168.32.102 | rpbfunVPEzw | |
| 03 | 192.168.32.103 | 1EPfPX8kfw8 | |
| 04 | 192.168.32.104 | zvgyLM5yieU | |
| 05 | 192.168.32.105 | QOc046GoeSc | |
| 06 | 192.168.32.106 | 2o8j6eb0L6s | |
| 07 | 192.168.32.107 | Oa1tlxPEVSQ | |
| 08 | 192.168.32.108 | fY4yznpUZ48 | |
| 09 | 192.168.32.109 | YlgGCyjo9bY | |
| 10 | 192.168.32.110 | GcTnFsp1mHI | |
| 11 | 192.168.32.111 | 6z88m9oEZio | |
| 12 | 192.168.32.112 | EggJqL2kWkA | |
| 13 | 192.168.32.113 | O11eckHFdVE | |
| 14 | 192.168.32.114 | reI0SecUEhI | |
| 15 | 192.168.32.115 | crozxT8mA44 | |
| 16 | 192.168.32.116 | 0nP4VZsvr2Q | |
| 17 | 192.168.32.117 | Gm2gNqPGzLA | |
| 19 | 192.168.32.119 | 6tpukvRVugU | (no laptop 18) |
| x20 | 192.168.32.120 | rwLYnKUNd1M | old 04, spare/retired |
aether_api_key: all laptops share a single key per event deployment. The key is created in
the Aether admin panel before the show and deleted after. Check builds/seed.json for the
current key, or create a new one in Aether (Core → Accounts or Events → Devices API key section)
before each deployment.
⚙️ Configuration
The application requires a seed.json file to identify the device and connect to the Aether API.
1. Seed Configuration
Location: ~/seed.json (user's home directory — external to the app bundle by design)
This file is intentionally kept outside the application bundle so it can be edited per-device
without re-signing or repackaging the app. On macOS this is /Users/speaker ready/seed.json.
Seed file format:
{
"event_device_id": "tFLL1fLQfnk",
"aether_api_key": "YOUR_API_KEY",
"primary_api_base_url": "https://api.oneskyit.com",
"backup_api_base_url": "https://bak-api.oneskyit.com",
"onsite_api_base_url": null
}
event_device_id is the id_random from the Aether event_device record for that physical
laptop — see the Device Reference table above. aether_api_key is a shared key created per
event deployment and deleted after the show.
2. Development Setup
npm install
npm start # Compiles TypeScript (tsc) then launches Electron
3. File Cache Layout
Presentation files are cached locally under hash_prefix_length-char subdirectories (default: 2):
[local_file_cache_path]/
4a/
4a228ef8ac1a...sha256hash...file
1d/
1d720916a831...sha256hash...file
Important: hash_prefix_length must be consistent. If it changes, files in old directories
become orphaned and will be re-downloaded. The default is 2 and should not be changed unless
explicitly coordinated across all devices.
🌉 The Native Bridge (aetherNative)
The bridge is exposed to the renderer via contextBridge. It can be accessed in the web UI via window.aetherNative.
Design principle: The Electron app is a thin OS primitive layer. Business logic (which script runs for which file type, how to sequence operations, etc.) belongs in the SvelteKit/Svelte side where it can be changed without a rebuild and redeployment. Electron handlers should rarely need to change.
File Cache
| Method | Description |
|---|---|
check_cache({cache_root, hash, hash_prefix_length?, verify_hash?}) |
Checks if a file exists in the hashed cache. verify_hash: true re-hashes the file to confirm integrity. |
download_to_cache({url, cache_root, hash, api_key, account_id, hash_prefix_length?}) |
Streams a file from the API into the hashed cache. Verifies SHA-256 integrity before finalizing. |
copy_from_cache_to_temp({cache_root, hash, temp_root, filename, hash_prefix_length?}) |
Preferred primitive. Copies cached file to temp dir with original filename. Returns { success, path }. The Svelte caller decides what to do next. |
launch_from_cache({cache_root, hash, temp_root, filename, hash_prefix_length?, script_template?}) |
Combines copy + launch. If script_template is provided, runs it (AppleScript or shell: prefixed command) instead of hardcoded extension logic. Falls back to built-in defaults when null. |
Shell & OS
| Method | Description |
|---|---|
run_cmd({cmd, timeout?, return_stdout?}) |
Async shell command execution. |
run_cmd_sync({cmd}) |
Synchronous shell command execution. |
run_osascript(script) |
Hardened. Runs AppleScript via temp .scpt file — handles multi-line scripts and paths with spaces/special characters correctly. macOS only. |
open_folder(path) |
Opens a directory in Finder / system file manager. |
open_local_file_v2(path) |
Opens a file with its default OS application. |
open_external({url, app?}) |
Opens a URL in Chrome, Firefox, or the default browser. |
kill_processes({process_name_li}) |
Terminates processes by name. macOS/Linux: pkill -f. Windows: taskkill /F. |
Presentations (Phase 5 — legacy specialized handlers)
| Method | Description |
|---|---|
launch_presentation({path, app?, os?}) |
Platform-aware launcher. Resolves [home]/[tmp] placeholders. Note: uses legacy -e flag for AppleScript; prefer copy_from_cache_to_temp + run_osascript for new flows. |
control_presentation({app, action}) |
Slide navigation (next/prev/start/stop) for PowerPoint or Keynote via AppleScript. macOS only. |
System Management (Phase 5)
| Method | Description |
|---|---|
get_device_config() |
Returns hydrated device config injected at startup from seed.json + API. |
get_device_info() |
Returns OS metadata: platform, hostname, IPs, CPU count, free RAM, home/tmp paths. |
window_control({action, value?}) |
Electron window: maximize, minimize, restore, close, fullscreen, kiosk, devtools, reload. |
set_wallpaper({path}) |
Sets desktop wallpaper. macOS (AppleScript) + Linux (gsettings/Gnome). |
power_control({action}) |
Shutdown, reboot, or sleep. macOS + Linux. Requires sudo for shutdown/reboot. |
set_display_layout({mode, configStr?}) |
Mirror/extend displays via bundled displayplacer binary. macOS only. |
manage_recording({action, options?}) |
Screen recording via bundled aperture binary. macOS only. |
update_app(args) |
Stub. Downloads update package but does not install. Not functional. |
list_tools() |
Returns a self-describing manifest of all available bridge functions. |
Example Usage (preferred composable pattern)
import * as native from '$lib/electron/electron_relay';
// Step 1: copy the cached file to temp and get the resolved path
const copy = await native.copy_from_cache_to_temp({
cache_root: $ae_loc.local_file_cache_path,
hash: event_file_obj.hash_sha256,
temp_root: $ae_loc.host_file_temp_path,
filename: event_file_obj.filename
});
if (!copy.success) { /* handle error */ return; }
// Step 2: run whatever script/command you want with that path
// Option A — AppleScript (macOS):
await native.run_osascript(`
tell application "Microsoft PowerPoint"
activate
open (POSIX file "${copy.path}")
delay 3
end tell
`);
// Option B — shell command:
await native.run_cmd({ cmd: `open "${copy.path}"` });
// Option C — use a template from device config (data-driven, no rebuild needed):
const template = $ae_loc.native_device?.launch_scripts?.pptx;
if (template) {
const script = template.replace(/\{\{path\}\}/g, copy.path);
await native.run_osascript(script);
}
Configurable Launch Scripts (no rebuild needed)
launch_from_cache and launcher_file_cont.svelte support per-extension script templates
stored in event_device.data_json.launch_scripts. Keys are lowercase extensions (pptx, key,
pdf, etc.); default is a catch-all. Templates use {{path}} as the file path placeholder.
AppleScript strings run via run_osascript; prefix with shell: for shell commands.
See documentation/PROJECT__AE_Events_Launcher_Native_integration.md Section 8 for full details.
🛠️ Development
- Preload: Logic defined in
src/preload/index.ts. - Handlers: OS-level logic in
src/main/shell_handlers.ts,src/main/file_handlers.ts, andsrc/main/system_handlers.ts. - Types: Shared TypeScript interfaces in
src/shared/types.ts.
Build Requirements (System Dependencies)
bsdtar (libarchive) must be present on the build host. It is used by the postinstall patch
to work around a hard hang in @electron/packager 20 on Node 26.
| OS | Install |
|---|---|
| Arch Linux | sudo pacman -S libarchive |
| macOS | brew install libarchive (or use the system bsdtar included in Xcode CLT) |
| Ubuntu/Debian | sudo apt install libarchive-tools |
Why bsdtar — Node 26 Packaging Bug
Symptom: npm run package:mac or npm run package:linux prints
Packaging app for platform ... using electron v42.x then hangs forever (or exits 0 with no output).
Root cause: yauzl 2.10.0 (used by extract-zip inside @electron/packager 20) creates
read streams that never emit data events under Node 26. The zip extraction call blocks
indefinitely.
Fix: scripts/patch-packager-unzip.js (run automatically via postinstall) replaces the
extractElectronZip function in node_modules/@electron/packager/dist/unzip.js with a
child_process.execSync call to bsdtar -xf. bsdtar was chosen over 7z because 7z
refuses to extract macOS .app bundles that contain symlinks chained through other symlinks
(e.g. Electron Framework.framework/Libraries → Versions/Current/Libraries, where
Versions/Current is itself a symlink).
This patch is re-applied automatically on every npm install via the postinstall hook.
If a future release of @electron/packager or extract-zip fixes Node 26 compatibility,
remove the postinstall line from package.json and delete scripts/patch-packager-unzip.js.