3 Commits

Author SHA1 Message Date
Scott Idem
7693b12aeb Clarify native template bridge contract 2026-05-13 11:44:36 -04:00
Scott Idem
72d928f907 Rename launch payload to native_template 2026-05-13 11:28:09 -04:00
Scott Idem
9b98b454fd Align launcher terminology docs 2026-05-13 11:25:55 -04:00
6 changed files with 110 additions and 215 deletions

View File

@@ -10,6 +10,22 @@ This application serves as the "Native Mode" runtime for Aether podiums and devi
- **Hardware Telemetry:** Direct access to CPU, RAM, and Network interface data.
- **Remote Control:** Slide navigation and application control via WebSocket intents.
## Launcher Terminology
Use these terms consistently when working on the launcher bridge:
- **Launch Profiles**: the Svelte-side map keyed by file extension.
- **Launch Profile**: one resolved config object selected from that map for a file.
- **Native Template**: the single AppleScript or shell command string Electron executes after
the file has been copied to temp. This is an implementation detail of the bridge.
In short, the profiles are the policy; the launch profile is the selected policy entry; the
native template is the executable string produced by that policy.
Do not use `launch_scripts` as the public/config-facing term. If that wording appears in old
comments or generated output, treat it as stale naming drift and update it to `launch_profiles`
when referring to the Svelte-side map or `native_template` when referring to the resolved string.
## 🖥️ Onsite Deployment
**Current hardware:** MacBook Air 2018 — Intel x64. All current deployments use `aether_launcher-darwin-x64`.
@@ -250,7 +266,7 @@ to change.
| `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`. |
| `launch_from_cache({cache_root, hash, temp_root, filename, hash_prefix_length?, native_template?})` | Combines copy + launch. The Svelte side resolves the Launch Profile to a single `native_template` string (AppleScript or `shell:` prefixed command). If no template is supplied, it returns an error. |
### Shell & OS
@@ -313,18 +329,19 @@ await native.run_osascript(`
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;
const template = $ae_loc.native_device?.launch_profiles?.pptx;
if (template) {
const script = template.replace(/\{\{path\}\}/g, copy.path);
await native.run_osascript(script);
}
```
### Configurable Launch Scripts (no rebuild needed)
### Configurable Launch Profiles (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.
`launch_profiles` is the Svelte-side map stored in `event_device.data_json.launch_profiles`.
`launch_from_cache` receives the resolved `native_template` string, not the profile map.
Keys are lowercase extensions (`pptx`, `key`, `pdf`, etc.); `default` is a catch-all.
Profiles 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.

View File

@@ -130,7 +130,7 @@ function registerFileHandlers() {
endpoints_in_progress = endpoints_in_progress.filter(e => e !== url);
}
});
electron_1.ipcMain.handle('native:launch-from-cache', async (event, { cache_root, hash, temp_root, filename, hash_prefix_length = 2, script_template = null }) => {
electron_1.ipcMain.handle('native:launch-from-cache', async (event, { cache_root, hash, temp_root, filename, hash_prefix_length = 2, native_template = null }) => {
try {
const source = get_organized_hashed_path(cache_root, hash, hash_prefix_length);
const expanded_temp = (0, file_utils_1.expandPath)(temp_root);
@@ -143,118 +143,46 @@ function registerFileHandlers() {
fs.mkdirSync(expanded_temp, { recursive: true });
// 1. Copy the file to temp folder with original name
fs.copyFileSync(source, target);
// 2a. Data-driven script override (no rebuild needed for script changes).
// Set via event_device.data_json.launch_scripts or $events_loc.launcher.launch_scripts.
// 2a. Data-driven launcher template (no rebuild needed for config changes).
// Svelte resolves a Launch Profile to a single native_template string.
// Format: AppleScript string with {{path}} placeholder, OR "shell:<cmd> {{path}}"
if (script_template) {
const resolved = script_template.replace(/\{\{path\}\}/g, target);
if (resolved.startsWith('shell:')) {
const cmd = resolved.slice(6).trim();
console.log(`Native: Running custom shell script for ${filename}`);
return new Promise((resolve_fn) => {
(0, child_process_1.exec)(cmd, (err, stdout, stderr) => {
if (err)
resolve_fn({ success: false, error: err.message, stderr: stderr.trim() });
else
resolve_fn({ success: true, stdout: stdout.trim() });
});
});
}
else {
// Treat as AppleScript — write to temp .scpt file (same hardened approach used below)
console.log(`Native: Running custom AppleScript for ${filename}`);
const tmp_script_path = path.join(os.tmpdir(), `ae_launch_${Date.now()}.scpt`);
return new Promise((resolve_fn) => {
try {
fs.writeFileSync(tmp_script_path, resolved.trim());
}
catch (e) {
resolve_fn({ success: false, error: `Failed to write AppleScript temp file: ${e.message}` });
return;
}
(0, child_process_1.exec)(`osascript "${tmp_script_path}"`, (err) => {
try {
fs.unlinkSync(tmp_script_path);
}
catch { }
if (err)
resolve_fn({ success: false, error: err.message });
else
resolve_fn({ success: true });
});
});
}
if (!native_template) {
return { success: false, error: 'No native template configured for this file' };
}
// 2b. Determine file type (legacy hardcoded launch logic — used when no script_template provided)
const ext = path.extname(filename).toLowerCase().replace('.', '');
const is_pres = ['pptx', 'ppt', 'key', 'pdf', 'odp'].includes(ext);
// 3. Hardcoded launch (legacy — still the default when no script_template is configured)
if (is_pres) {
if (os.platform() === 'linux') {
console.log(`Native: Launching LibreOffice (--impress) for ${target}`);
return new Promise((resolve) => {
(0, child_process_1.exec)(`libreoffice --impress "${target}"`, (err) => {
if (err)
resolve({ success: false, error: err.message });
else
resolve({ success: true });
});
const resolved = native_template.replace(/\{\{path\}\}/g, target);
if (resolved.startsWith('shell:')) {
const cmd = resolved.slice(6).trim();
console.log(`Native: Running custom shell template for ${filename}`);
return new Promise((resolve_fn) => {
(0, child_process_1.exec)(cmd, (err, stdout, stderr) => {
if (err)
resolve_fn({ success: false, error: err.message, stderr: stderr.trim() });
else
resolve_fn({ success: true, stdout: stdout.trim() });
});
}
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 3
end tell
tell application "System Events"
keystroke return using command down
end tell
`;
}
if (script) {
console.log(`Native: Launching ${ext} via AppleScript for ${target}`);
// Write to a temp .scpt file instead of passing via -e flag.
// The -e approach breaks on multi-line scripts and paths with spaces or quotes.
const tmp_script_path = path.join(os.tmpdir(), `ae_launch_${Date.now()}.scpt`);
return new Promise((resolve) => {
try {
fs.writeFileSync(tmp_script_path, script.trim());
}
catch (e) {
resolve({ success: false, error: `Failed to write AppleScript temp file: ${e.message}` });
return;
}
(0, child_process_1.exec)(`osascript "${tmp_script_path}"`, (err) => {
try {
fs.unlinkSync(tmp_script_path);
}
catch { }
if (err)
resolve({ success: false, error: err.message });
else
resolve({ success: true });
});
});
}
}
});
}
// 4. Default Fallback
await electron_1.shell.openPath(target);
return { success: true };
console.log(`Native: Running custom AppleScript template for ${filename}`);
const tmp_script_path = path.join(os.tmpdir(), `ae_launch_${Date.now()}.scpt`);
return new Promise((resolve_fn) => {
try {
fs.writeFileSync(tmp_script_path, resolved.trim());
}
catch (e) {
resolve_fn({ success: false, error: `Failed to write AppleScript temp file: ${e.message}` });
return;
}
(0, child_process_1.exec)(`osascript "${tmp_script_path}"`, (err) => {
try {
fs.unlinkSync(tmp_script_path);
}
catch { }
if (err)
resolve_fn({ success: false, error: err.message });
else
resolve_fn({ success: true });
});
});
}
catch (error) {
return { success: false, error: error.message };
@@ -265,7 +193,7 @@ function registerFileHandlers() {
// run_osascript, run_cmd, open_local_file, etc.
//
// This is the preferred building block for custom launch flows. Use launch_from_cache
// when the built-in hardcoded logic is sufficient; use copy_from_cache_to_temp when
// when Svelte has already resolved a native template; use copy_from_cache_to_temp when
// you want full control over what happens after the file lands in temp.
electron_1.ipcMain.handle('native:copy-from-cache-to-temp', async (event, { cache_root, hash, temp_root, filename, hash_prefix_length = 2 }) => {
try {

File diff suppressed because one or more lines are too long

View File

@@ -7,6 +7,20 @@
- We now know the API side was not the root cause. The bootstrap request shape in `src/main/api_client.ts` was wrong and has been corrected.
- The packaging blocker has been diagnosed and fixed (see below).
## Launcher Terminology Cleanup
- Align the native docs/comments with the Svelte-side terminology: **Launch Profile** = the
Svelte config object keyed by extension; **Native Template** = the AppleScript or shell
string Electron actually executes after the file lands in temp.
- Update `src/main/file_handlers.ts` comments and any bridge-facing wording so they describe the
resolved `native_template` string accurately. `launch_profiles` is only the Svelte-side map;
the IPC payload is the single executable string. Do not reintroduce `launch_scripts` as the
public term.
- Keep the source of truth in Svelte. Electron should remain a thin executor/copy layer.
- After source/docs updates, rebuild/regenerate `dist/main/file_handlers.js` from source; do not
hand-edit the generated file.
- If any parameter names remain awkward in source, preserve runtime behavior first and rename
only when the signature ripple is understood.
## What Was Fixed
- Updated the native bootstrap flow to use the direct `site_domain/search` request body expected by API V3.
- Standardized the account-bypass header to `x-no-account-id: bypass` where that narrow bypass is intended.

View File

@@ -1,4 +1,4 @@
import { ipcMain, shell } from 'electron';
import { ipcMain } from 'electron';
import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
@@ -100,7 +100,7 @@ export function registerFileHandlers() {
}
});
ipcMain.handle('native:launch-from-cache', async (event, { cache_root, hash, temp_root, filename, hash_prefix_length = 2, script_template = null }) => {
ipcMain.handle('native:launch-from-cache', async (event, { cache_root, hash, temp_root, filename, hash_prefix_length = 2, native_template = null }) => {
try {
const source = get_organized_hashed_path(cache_root, hash, hash_prefix_length);
const expanded_temp = expandPath(temp_root);
@@ -117,105 +117,41 @@ export function registerFileHandlers() {
// 1. Copy the file to temp folder with original name
fs.copyFileSync(source, target);
// 2a. Data-driven script override (no rebuild needed for script changes).
// Set via event_device.data_json.launch_scripts or $events_loc.launcher.launch_scripts.
// 2a. Data-driven launcher template (no rebuild needed for config changes).
// Svelte resolves a Launch Profile to a single native_template string.
// Format: AppleScript string with {{path}} placeholder, OR "shell:<cmd> {{path}}"
if (script_template) {
const resolved = (script_template as string).replace(/\{\{path\}\}/g, target);
if (resolved.startsWith('shell:')) {
const cmd = resolved.slice(6).trim();
console.log(`Native: Running custom shell script for ${filename}`);
return new Promise((resolve_fn) => {
exec(cmd, (err, stdout, stderr) => {
if (err) resolve_fn({ success: false, error: err.message, stderr: stderr.trim() });
else resolve_fn({ success: true, stdout: stdout.trim() });
});
});
} else {
// Treat as AppleScript — write to temp .scpt file (same hardened approach used below)
console.log(`Native: Running custom AppleScript for ${filename}`);
const tmp_script_path = path.join(os.tmpdir(), `ae_launch_${Date.now()}.scpt`);
return new Promise((resolve_fn) => {
try {
fs.writeFileSync(tmp_script_path, resolved.trim());
} catch (e: any) {
resolve_fn({ success: false, error: `Failed to write AppleScript temp file: ${e.message}` });
return;
}
exec(`osascript "${tmp_script_path}"`, (err) => {
try { fs.unlinkSync(tmp_script_path); } catch {}
if (err) resolve_fn({ success: false, error: err.message });
else resolve_fn({ success: true });
});
});
}
if (!native_template) {
return { success: false, error: 'No native template configured for this file' };
}
// 2b. Determine file type (legacy hardcoded launch logic — used when no script_template provided)
const ext = path.extname(filename).toLowerCase().replace('.', '');
const is_pres = ['pptx', 'ppt', 'key', 'pdf', 'odp'].includes(ext);
// 3. Hardcoded launch (legacy — still the default when no script_template is configured)
if (is_pres) {
if (os.platform() === 'linux') {
console.log(`Native: Launching LibreOffice (--impress) for ${target}`);
return new Promise((resolve) => {
exec(`libreoffice --impress "${target}"`, (err) => {
if (err) resolve({ success: false, error: err.message });
else resolve({ success: true });
});
const resolved = native_template.replace(/\{\{path\}\}/g, target);
if (resolved.startsWith('shell:')) {
const cmd = resolved.slice(6).trim();
console.log(`Native: Running custom shell template for ${filename}`);
return new Promise((resolve_fn) => {
exec(cmd, (err, stdout, stderr) => {
if (err) resolve_fn({ success: false, error: err.message, stderr: stderr.trim() });
else resolve_fn({ success: true, stdout: stdout.trim() });
});
}
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 3
end tell
tell application "System Events"
keystroke return using command down
end tell
`;
}
if (script) {
console.log(`Native: Launching ${ext} via AppleScript for ${target}`);
// Write to a temp .scpt file instead of passing via -e flag.
// The -e approach breaks on multi-line scripts and paths with spaces or quotes.
const tmp_script_path = path.join(os.tmpdir(), `ae_launch_${Date.now()}.scpt`);
return new Promise((resolve) => {
try {
fs.writeFileSync(tmp_script_path, script.trim());
} catch (e: any) {
resolve({ success: false, error: `Failed to write AppleScript temp file: ${e.message}` });
return;
}
exec(`osascript "${tmp_script_path}"`, (err) => {
try { fs.unlinkSync(tmp_script_path); } catch {}
if (err) resolve({ success: false, error: err.message });
else resolve({ success: true });
});
});
}
}
});
}
// 4. Default Fallback
await shell.openPath(target);
return { success: true };
console.log(`Native: Running custom AppleScript template for ${filename}`);
const tmp_script_path = path.join(os.tmpdir(), `ae_launch_${Date.now()}.scpt`);
return new Promise((resolve_fn) => {
try {
fs.writeFileSync(tmp_script_path, resolved.trim());
} catch (e: any) {
resolve_fn({ success: false, error: `Failed to write AppleScript temp file: ${e.message}` });
return;
}
exec(`osascript "${tmp_script_path}"`, (err) => {
try { fs.unlinkSync(tmp_script_path); } catch {}
if (err) resolve_fn({ success: false, error: err.message });
else resolve_fn({ success: true });
});
});
} catch (error: any) {
return { success: false, error: error.message };
}
@@ -226,7 +162,7 @@ export function registerFileHandlers() {
// run_osascript, run_cmd, open_local_file, etc.
//
// This is the preferred building block for custom launch flows. Use launch_from_cache
// when the built-in hardcoded logic is sufficient; use copy_from_cache_to_temp when
// when Svelte has already resolved a native template; use copy_from_cache_to_temp when
// you want full control over what happens after the file lands in temp.
ipcMain.handle('native:copy-from-cache-to-temp', async (event, { cache_root, hash, temp_root, filename, hash_prefix_length = 2 }) => {
try {

View File

@@ -11,7 +11,7 @@ export interface AetherNativeBridge {
get_device_config: () => Promise<any>;
get_jwt: () => Promise<string | null>;
get_device_info: () => Promise<any>;
// Shell Handlers
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}>;
@@ -24,7 +24,7 @@ export interface AetherNativeBridge {
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}>;
copy_from_cache_to_temp: (args: {cache_root: string, hash: string, temp_root: string, filename: string, hash_prefix_length?: number}) => Promise<{success: boolean, path?: string, error?: string}>;
launch_from_cache: (args: {cache_root: string, hash: string, temp_root: string, filename: string, hash_prefix_length?: number, script_template?: string}) => Promise<{success: boolean, error?: string}>;
launch_from_cache: (args: {cache_root: string, hash: string, temp_root: string, filename: string, hash_prefix_length?: number, native_template?: string}) => 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}>;