32 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
Scott Idem
ec29a576d5 feat(deploy): add --fix-accessibility flag + document TCC requirement
macOS invalidates Accessibility permission whenever the app binary
changes (code signature shifts on each build). New --fix-accessibility
flag runs tccutil reset + a sudo sqlite3 TCC grant via SSH after the
.app is synced. Falls back gracefully if sqlite3 grant fails (SIP or
missing sudoers), logging a warning with a pointer to the manual steps.

README documents the symptom, manual fix, sudoers one-time setup,
and bundle ID (com.electron.aetherlauncher).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 14:09:34 -04:00
Scott Idem
1f90c819a0 docs(bridge): update types + docs for set_display_layout configStr
- Add set_display_layout (+ other missing system handler methods) to
  AetherNativeBridge interface in types.ts
- README: clarify configStr source (event_device.data_json) and
  no-op behaviour when absent
- TODO_AGENTS: correct 7z→bsdtar throughout; mark Electron-side
  set_display_layout items done; remove completed bsdtar doc item

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 13:36:38 -04:00
Scott Idem
9f76d6b7f4 Noting the warning about icons missing 2026-05-11 17:24:50 -04:00
Scott Idem
ca4fddd57f fix(bridge): expose copy_from_cache_to_temp + harden launch_presentation
copy_from_cache_to_temp IPC handler was registered in file_handlers.ts
but never added to the preload bridge, making it unreachable from Svelte
despite being the documented preferred primitive for custom launch flows.

launch_presentation was the last handler still using osascript -e with
inline path injection. Converted to the temp-.scpt-file approach already
used by run_osascript and launch_from_cache — prevents breakage on
presentation filenames with spaces, quotes, or parentheses.

Also adds a pre-copy existence check to launch_from_cache so a missing
cache entry returns a meaningful error instead of a raw ENOENT.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 17:15:36 -04:00
Scott Idem
c5a368aee5 chore: ignore builds.tar.gz 2026-05-11 17:07:22 -04:00
Scott Idem
b8199375a9 chore: ignore .playwright-mcp session artifacts 2026-05-11 16:59:47 -04:00
Scott Idem
bab08cd8a7 fix(packaging): workaround yauzl/Node 26 hang + fix API bootstrap contract
Packaging was silently hanging forever because yauzl 2.10.0 read streams
emit no data events under Node 26, causing extract-zip to block indefinitely
inside @electron/packager 20. Fix: postinstall script patches
@electron/packager/dist/unzip.js to use bsdtar (libarchive) instead.
bsdtar was chosen over 7z because 7z refuses chained symlinks in macOS
.app framework bundles. Both package:linux and package:mac now produce
correct output.

Also corrects the V3 API bootstrap contract in api_client.ts:
- SearchQuery body was wrapped in an extra {search_query: ...} layer — removed
- x-no-account-id header standardised to 'bypass'
- Redundant x-no-account-id removed from file download headers
- Smoke test rewritten to validate the real two-step bootstrap path

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 16:48:15 -04:00
Scott Idem
36aed19169 feat(launcher): thin primitive architecture + run_osascript hardening
- file_handlers: add script_template param to native:launch-from-cache
  (AppleScript or shell: prefix; falls back to hardcoded defaults when null)
- file_handlers: add native:copy-from-cache-to-temp as composable primitive
  (copies cached file to temp, returns path — caller handles launch logic)
- shell_handlers: harden native:run-osascript with temp .scpt file approach
  (replaces -e flag; handles multi-line scripts and paths with special chars)
- README: rewrite Native Bridge section — full method table, composable
  pattern example, configurable launch scripts note
- deploy/devices.conf: update IPs for devices 03-06, uncomment entries
2026-05-11 13:40:05 -04:00
Scott Idem
5b59dbc2da Old files for reference to help with AppleScript and related. 2026-04-20 17:02:46 -04:00
Scott Idem
002c27e73c fix(file_handlers): use keystroke Cmd+Return to start pptx slideshow
Replace unreliable AppleScript PowerPoint API (run slide show of settings)
with System Events keystroke approach, matching proven behavior from the
old MasterKey app. Opens the file, waits 3s for load, then sends Cmd+Return
to start the slideshow from slide 1.
2026-04-20 17:01:04 -04:00
Scott Idem
3feaf1bbc3 feat(deploy): add deploy script and per-device config files
deploy/deploy.sh — automated deploy: arch detection, scp .app, write
seed.json, verify. Supports single laptop, list, or "all"; --seed-only
flag skips .app copy for key-rotation runs.

deploy/devices.conf — all 19 laptops (num / IP / event_device_id).
deploy/event.env.example — template for gitignored event.env (API key).

README updated: deploy/ table, script usage, manual steps moved to
reference section.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-20 15:02:15 -04:00
Scott Idem
b8b7b253bb assets: add logo and icon files for packaging
Add OSIT logo images (PNG, ICO) and .icns icon used by electron-packager
when producing macOS .app bundles.
2026-04-20 14:27:05 -04:00
Scott Idem
b3f59b7bf5 docs: add onsite deployment guide and device reference table
- Step-by-step SSH deployment instructions (build, copy .app, write seed.json)
- Multi-laptop deployment loop example
- Device reference table: all 19 laptops with IPs and event_device_ids
- Clarify seed.json location, format, and per-event API key workflow
- Document architecture detection (x64 vs arm64) and SSH key setup
2026-04-20 14:26:54 -04:00
Scott Idem
01797f28aa fix: use correct protocol for app URL and suppress DevTools in production
- Use app_base_url from the device record directly instead of always
  falling back to localhost; use https for real domains and http for
  localhost dev URLs only.
- Suppress DevTools in packaged .app builds (app.isPackaged check)
  so they only open during development.
2026-04-20 14:26:43 -04:00
Scott Idem
2af6b3954b fix: write AppleScript to temp .scpt file instead of using -e flag
The -e flag approach breaks on multi-line AppleScript and on file paths
containing spaces or quote characters. Write the script to a temp file
in os.tmpdir() and invoke osascript with the file path instead. The
temp file is cleaned up after execution.
2026-04-20 14:26:34 -04:00
Scott Idem
5a5814b2bc build: add electron-packager and package:mac script
Add electron-packager as a dev dependency and a package:mac npm script
that compiles TypeScript and packages dual-arch (x64, arm64) macOS builds
into builds/ using the OSIT icon.
2026-04-20 14:24:17 -04:00
Scott Idem
0ebfcd18bb chore: ignore builds/ directory and seed config files
builds/ contains large compiled .app bundles (500MB+) not suitable for git.
seed_config.json contains API keys and must not be committed.
2026-04-20 14:24:06 -04:00
Scott Idem
2ad6bce8db fix: repair file_handlers.ts backtick escaping; update README
file_handlers.ts had AI-generated escaped backticks (\`) and template
literal dollar signs (\${) throughout, causing TypeScript compile errors
("Invalid character", "Unterminated template literal"). Fixed all 15
affected lines.

README updated: corrected seed config path from resources/seed_config.json
to ~/seed.json (external to app bundle by design), added explanation of
why it's kept external (no re-signing needed per device), and documented
the 2-char hash prefix cache layout with consistency warning.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-11 11:50:06 -04:00
Scott Idem
7c0bb6719d feat(file): implement hardened caching with SHA-256 verification
- Added .tmp -> .file download pattern with integrity checks.\n- Implemented auto-purge for stale temp files (>5 mins).\n- Added verify_hash support to check-cache handler.\n- Improved error handling for network interruptions.
2026-02-10 14:07:45 -05:00
Scott Idem
fb8af70742 feat: implement Phase 5 system handlers (automation, power, recording)
- Implement window control, wallpaper reset, and power management.
- Add Aperture recording wrapper and displayplacer layout control.
- Add self-update logic stub for local/remote sources.
- Register and expose handlers via context bridge.
2026-01-30 11:34:53 -05:00
Scott Idem
3d7aa1ab92 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.
2026-01-26 16:18:00 -05:00
Scott Idem
7784f7f2a3 chore: add VS Code workspace configuration with markdown linting overrides 2026-01-26 15:19:20 -05:00
Scott Idem
083fc56337 feat(bridge): implement presentation-aware handover and robust placeholder resolution
- Upgraded launch_from_cache to automatically trigger LibreOffice/AppleScript launchers after file copy.
- Hardened expandPath to resolve [home] and [tmp] placeholders anywhere in strings via global regex.
- Enhanced get-device-info telemetry to provide absolute home and tmp paths to the UI.
- Exposed native:launch-presentation in preload and implemented explicit LibreOffice (--impress) support on Linux.
2026-01-26 15:12:11 -05:00
Scott Idem
e942b234c4 Fix: Finalize Phase 3 OS interactions and telemetry bridge
- Implemented get_device_info for OS/Network telemetry.
- Added run_cmd_sync using execSync for blocking-style commands.
- Implemented kill_processes (plural) to support multiple process termination.
- Standardized open_local_file_v2 and all bridge methods to snake_case.
- Synchronized preload and main handlers with SvelteKit relay expectations.
2026-01-23 17:23:14 -05:00
Scott Idem
f6875acc72 Enhance: Support flexible hash prefix length and organized caching
- Updated organized path logic to accept variable prefix length.
- Refactored IPC handlers to pass hash_prefix_length from frontend.
- Standardized parameter structures for check-cache and download-to-cache.
2026-01-23 16:56:10 -05:00
Scott Idem
280de213c1 Fix: Standardize Electron bridge and implement robust caching
- Refactored all IPC methods and parameters to snake_case for consistency with SvelteKit.
- Implemented exhaustive background caching engine with download locking.
- Reverted to legacy-proven flat hash storage pattern (hash.file).
- Added axios for reliable stream-based binary downloads.
- Updated preload and main handlers to support recursive room data fetching.
2026-01-23 16:30:23 -05:00
Scott Idem
30db989b2c Enhance: Implement dynamic launcher URL and restore package.json
- Added logic to construct launcher URL based on hydrated device context.
- Implemented dev/production host fallback for demo.localhost.
- Restored missing package.json with proper start and build scripts.
- Finalized IPC handlers for seed and device configuration.
2026-01-23 14:08:31 -05:00
Scott Idem
0497f5767b Initial scaffold for Aether Native V3 Electron Launcher 2026-01-23 13:54:20 -05:00
Scott Idem
fdbd12b64f First iteration of the conversion to use SvelteKit. 2024-08-14 19:26:39 -04:00
96 changed files with 5789 additions and 8052 deletions

5
.gitignore vendored
View File

@@ -25,3 +25,8 @@ development/
file_cache/
aether_native_app_config.json
# package-lock.json
builds/
resources/seed_config.json
event.env
.playwright-mcp/
builds.tar.gz

Binary file not shown.

View File

@@ -1,8 +0,0 @@
git clone https://scott_idem@bitbucket.org/oneskyit/one-sky-it-app-native.git ~/OSIT_dev/aether_app_native
cd ~/OSIT_dev/aether_app_native/
npm update
npm start
git branch
git checkout development

384
README.md Normal file
View File

@@ -0,0 +1,384 @@
# 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.
## 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`.
**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)
```bash
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.
> **Note:** The build will print `WARNING: Could not find icon "..." with extension ".icon"`. This
> is cosmetic — `electron-packager` checks for several icon format variants and warns for each it
> doesn't find. The `.icns` file is correctly embedded as `electron.icns` inside the app bundle.
### Step 2 — Create event.env
```bash
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
```bash
# 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
# Deploy and re-grant Accessibility permission in one pass:
./deploy/deploy.sh --fix-accessibility 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.
### macOS Accessibility Permission
The launcher sends keystrokes to PowerPoint and Keynote via AppleScript. macOS requires
explicit **Accessibility** access for this. Every time a new `.app` binary is deployed, macOS
invalidates the stored permission because the code signature changes — even when rsync
updates in-place.
**Symptom:** Slide control silently fails; `osascript` eventually returns a permissions error.
**Manual fix** (GUI — always works):
1. System Settings → Privacy & Security → Accessibility
2. Find `aether_launcher` in the list → remove it ( button)
3. Re-add it (+ button → `/Applications/aether_launcher.app`) and toggle it on
**Automated fix** — use the `--fix-accessibility` deploy flag:
```bash
./deploy/deploy.sh --fix-accessibility 01 02 03
./deploy/deploy.sh --build --fix-accessibility all
```
This runs `tccutil reset` (no password required) then attempts a direct TCC database grant
via `sudo sqlite3`. The sqlite3 step requires NOPASSWD sudo on each Mac — one-time setup:
```bash
ssh "speaker ready"@192.168.32.1XX "sudo visudo -f /etc/sudoers.d/aether-tcc"
```
Add this line, then save:
```text
speaker ready ALL=(ALL) NOPASSWD: /usr/bin/sqlite3 /Library/Application\ Support/com.apple.TCC/TCC.db *
```
If the sqlite3 grant fails (SIP enabled, sudoers not configured), the script logs a warning
and falls back gracefully — a fresh permission prompt appears the first time the app uses
accessibility. The `--fix-accessibility` flag can be combined with any other flag.
**Long-term fix:** Code-sign the app with an Apple Developer certificate. A stable signature
means macOS never invalidates the permission on updates. Currently out of scope.
### Adding SSH key to a new laptop (first time only)
```bash
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:
```bash
# 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:
```json
{
"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
```bash
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):
```text
[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?, 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
| 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. `configStr` is the output of `displayplacer list` for that machine, stored in `event_device.data_json.displayplacer_config_mirror` / `displayplacer_config_extend`. Required — silently no-ops without it. |
| `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)
```typescript
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_profiles?.pptx;
if (template) {
const script = template.replace(/\{\{path\}\}/g, copy.path);
await native.run_osascript(script);
}
```
### Configurable Launch Profiles (no rebuild needed)
`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.
## 🛠️ Development
- **Preload:** Logic defined in `src/preload/index.ts`.
- **Handlers:** OS-level logic in `src/main/shell_handlers.ts`, `src/main/file_handlers.ts`, and `src/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`.

View File

@@ -1,73 +0,0 @@
{
"developer_tools": false,
"native_app_which_html": "default",
"native_app_index_path": "[home]/OSIT/native_app/app/index.html",
"native_app_index_url": "https://app.oneskyit.com/native/index.html",
"native_app_js_css_base_url": "https://demo.oneskyit.com",
"native_app_js_css_base_url_bak": "https://bak-demo.oneskyit.com",
"account_id": "",
"event_id": "",
"event_device_id": "soon_to_be_required",
"event_location_id": "",
"event_session_id": "",
"account_code": "",
"event_code": "",
"event_device_code": "eventually_use_code",
"event_location_code": "",
"event_session_code": "",
"app_root_path": "[home]/OSIT/native_app",
"api_protocol": "https",
"api_server": "api.oneskyit.com",
"api_port": 443,
"api_path": "",
"api_secret_key": "ABCD1234XYZ",
"api_protocol_backup": "https",
"api_server_backup": "bak-api.oneskyit.com",
"api_port_backup": 443,
"api_path_backup": "",
"api_secret_key_backup": "ABCD1234XYZ",
"access_control_allow_origin": "*",
"idb_name": "osit",
"local_file_cache_path": "[home]/OSIT/file_cache",
"host_file_temp_path": "[home]/OSIT/temp",
"display_arrangement": "mirror_and_extend",
"display_builtin_resolution": "",
"display_builtin_refresh": "",
"display_builtin_rotation": "",
"display_external_resolution": "",
"display_external_refresh": "",
"display_external_rotation": "",
"audio_out_volume": null,
"audio_in_volume": null,
"recording_fps": 30,
"recording_show_cursor": true,
"recording_highlight_clicks": false,
"recording_screen_id": null,
"recording_audio_device_id": null,
"known_builtin_screen_ids": [69732032, 69733952, 69733248],
"known_builtin_audio_device_ids": [ "AppleHDAEngineInput:1B,0,1,0:1", "BuiltInMicrophoneDevice" ],
"recording_video_codec": "h264",
"recording_path": "[home]/recordings",
"recording_base_filename": "recording",
"aperture_bin_path": null,
"recording_start_datetime": "2019-10-12 01:01:01Z",
"recording_stop_datetime": "2019-10-31 23:59:59Z",
"recordings_datetime": [
{ "start": "2019-10-11T09:50:00.00", "stop": "2019-10-11T10:15:00.00" },
{ "start": "2019-10-11T10:50:00.00", "stop": "2019-10-11T11:15:00.00" }
]
}

View File

@@ -1,8 +0,0 @@
{
"folders": [
{
"path": "."
}
],
"settings": {}
}

View File

@@ -0,0 +1,16 @@
{
"folders": [
{
"path": "."
}
],
"settings": {
"git.autofetch": true,
"markdownlint.config": {
"MD007": false,
"MD030": false,
"MD004": false,
"MD033": false
}
}
}

View File

@@ -1,28 +0,0 @@
body {
/* min-height: 100%;
height: 100%;
max-height: 100%; */
/* margin: .1em;
padding: .1em; */
}
section#Main-Body {
/* outline: solid thin red; */
/* min-height: 100%;
height: 100%;
max-height: 100%; */
}
section#Main-Nav-Menu {
/* min-height: 100%;
height: 100%;
max-height: 100%; */
}
section#Main-Content {
/* min-height: 100%;
height: 100%;
max-height: 100%; */
}

View File

@@ -1 +0,0 @@
launcher reset

View File

@@ -1 +0,0 @@
test txt

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

View File

@@ -1,47 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>One Sky IT's Aether App - External XXX</title>
<link rel="shortcut icon" type="image/png" href="img/favicon.ico">
<!-- Cascading Style Sheets (CSS) start -->
<link rel="preconnect" href="https://fonts.gstatic.com">
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans:ital,wght@0,400;0,700;1,400;1,700&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Roboto+Mono:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;1,100;1,200;1,300;1,400;1,500;1,600;1,700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css" integrity="sha512-1ycn6IcaQQ40/MKBW2W4Rhis/DbILU74C1vSrLJxCq57o941Ym01SwNsOMqvEBFlcgUa6xLiPY/NS5R+E6ztJQ==" crossorigin="anonymous" referrerpolicy="no-referrer" />
<!-- Cascading Style Sheets (CSS) end -->
<!-- JavaScript (JS) start -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/axios/1.1.3/axios.min.js" integrity="sha512-0qU9M9jfqPw6FKkPafM3gy2CBAvUWnYVOfNPDYKVuRTel1PrciTj+a9P3loJB+j0QmN2Y0JYQmkBBS8W+mbezg==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="https://webrtc.github.io/adapter/adapter-latest.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.2.0/socket.io.js" integrity="sha512-WL6WGKMPBiM9PnHRYIn5YEtq0Z8XP4fkVb4qy7PP4vhmYQErJ/dySyXuFIMDf1eEYCXCrQrMJfkNwKc9gsjTjA==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/dayjs/1.10.7/dayjs.min.js" integrity="sha512-bwD3VD/j6ypSSnyjuaURidZksoVx3L1RPvTkleC48SbHCZsemT3VKMD39KknPnH728LLXVMTisESIBOAb5/W0Q==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/localforage/1.10.0/localforage.min.js" integrity="sha512-+BMamP0e7wn39JGL8nKAZ3yAQT2dL5oaXWr4ZYlTGkKOaoXM/Yj7c4oy50Ngz5yoUutAG17flueD4F6QpTlPng==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<!-- JavaScript (JS) end -->
<script type="module">
import {check_and_copy_app_files} from 'https://dev-cmsc.oneskyit.com/srv/assets/js/init_bootstrap_app.js';
check_and_copy_app_files({overwrite: true});
</script>
<script src="script.js"></script>
<link rel="stylesheet" href="style.css">
<style>
</style>
</head>
<body id="Body-Container" class="body_container">
Loaded HTML
</body>
</html>

View File

@@ -1,362 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>One Sky IT's Aether App - External</title>
<link rel="shortcut icon" type="image/png" href="img/favicon.ico">
<!-- Cascading Style Sheets (CSS) start -->
<link rel="preconnect" href="https://fonts.gstatic.com">
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans:ital,wght@0,400;0,700;1,400;1,700&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Roboto+Mono:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;1,100;1,200;1,300;1,400;1,500;1,600;1,700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css" integrity="sha512-1ycn6IcaQQ40/MKBW2W4Rhis/DbILU74C1vSrLJxCq57o941Ym01SwNsOMqvEBFlcgUa6xLiPY/NS5R+E6ztJQ==" crossorigin="anonymous" referrerpolicy="no-referrer" />
<!-- Cascading Style Sheets (CSS) end -->
<!-- JavaScript (JS) start -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/axios/1.1.3/axios.min.js" integrity="sha512-0qU9M9jfqPw6FKkPafM3gy2CBAvUWnYVOfNPDYKVuRTel1PrciTj+a9P3loJB+j0QmN2Y0JYQmkBBS8W+mbezg==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="https://webrtc.github.io/adapter/adapter-latest.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.2.0/socket.io.js" integrity="sha512-WL6WGKMPBiM9PnHRYIn5YEtq0Z8XP4fkVb4qy7PP4vhmYQErJ/dySyXuFIMDf1eEYCXCrQrMJfkNwKc9gsjTjA==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/dayjs/1.10.7/dayjs.min.js" integrity="sha512-bwD3VD/j6ypSSnyjuaURidZksoVx3L1RPvTkleC48SbHCZsemT3VKMD39KknPnH728LLXVMTisESIBOAb5/W0Q==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/localforage/1.10.0/localforage.min.js" integrity="sha512-+BMamP0e7wn39JGL8nKAZ3yAQT2dL5oaXWr4ZYlTGkKOaoXM/Yj7c4oy50Ngz5yoUutAG17flueD4F6QpTlPng==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<!-- JavaScript (JS) end -->
<script type="module">
import {check_for_native_app_update, check_and_copy_app_files, check_and_get_updated_native_app_config} from 'https://dev-cmsc.oneskyit.com/srv/assets/js/init_bootstrap_app.js';
check_for_native_app_update({overwrite: false});
check_and_copy_app_files({overwrite: true});
check_and_get_updated_native_app_config({overwrite: false})
// let native_app_config = load_init_config();
// console.log('Native App Initial Config:', native_app_config);
// const native_app = require('./script');
// let native_app_config = native_app.load_init_config();
// console.log('Native App Initial Config:', native_app_config);
//
// let native_app_js_css_base_url = native_app_config.native_app_js_css_base_url;
//
// document.aether = {
// name: 'Aether Native App (Electron and Svelte)'
// };
// document.write(`<link rel="stylesheet" href="${native_app_js_css_base_url}/static/css/ae_css_variables.css" />`);
</script>
<script>
const native_app = require('./script');
let native_app_config = native_app.load_init_config();
console.log('Native App Initial Config:', native_app_config);
// native_app_config = native_app.load_full_config(native_app_config);
// console.log('Native App Full Config:', native_app_config);
let native_app_js_css_base_url = native_app_config.native_app_js_css_base_url;
document.aether = {
name: 'Aether Native App (Electron and Svelte)'
};
function is_online(uri) {
let xhr = new XMLHttpRequest();
xhr.open('GET',uri,false);
try {
xhr.send(null);
} catch (error) {
console.error(error);
}
if(xhr.status == 200) {
//is online
return xhr.responseText;
}
else {
//is offline
return null;
}
}
if (is_online(native_app_config.native_app_js_css_base_url)) {
console.log(`Appears to be live: ${native_app_config.native_app_js_css_base_url}`);
} else if (native_app_config.native_app_js_css_base_url_bak) {
console.log(`Appears to be live: ${native_app_config.native_app_js_css_base_url_bak}`);
native_app_js_css_base_url = native_app_config.native_app_js_css_base_url_bak;
} else if (is_online('https://app.oneskyit.com')) {
console.log(`Appears to be live: https://app.oneskyit.com`);
native_app_js_css_base_url = 'https://app.oneskyit.com';
} else if (is_online('https://oneskyit.com')) {
console.log(`One Sky IT appears to be online`);
native_app_js_css_base_url = 'https://oneskyit.com';
} else if (is_online('https://google.com')) {
console.log(`Google appears to be online`);
native_app_js_css_base_url = 'https://google.com';
} else {
console.log('We are offline!');
}
// console.log(`One Sky IT appears to be online ${is_online('https://oneskyit.com')}`);
// console.log(`Google appears to be online ${is_online('https://google.com')}`);
// console.log(is_online('https://oneskyit.com'));
// console.log(is_online('https://cmsc.oneskyit.com'));
// console.log(is_online('https://dev-cmsc.oneskyit.com'));
// console.log(is_online('https://google.com'));
</script>
<!-- One Sky IT Cascading Style Sheets (CSS) -->
<script>document.write(`<link rel="stylesheet" href="${native_app_js_css_base_url}/static/css/ae_css_variables.css" />`);</script>
<script>document.write(`<link rel="stylesheet" href="${native_app_js_css_base_url}/static/css/ae_utilities.css" />`);</script>
<script>document.write(`<link rel="stylesheet" href="${native_app_js_css_base_url}/static/css/ae_shared_components.css" />`);</script>
<script>document.write(`<link rel="stylesheet" href="${native_app_js_css_base_url}/static/css/ae_core_modules_components.css" />`);</script>
<script>document.write(`<link rel="stylesheet" href="${native_app_js_css_base_url}/static/css/ae_layout_base.css" />`);</script>
<script>document.write(`<link rel="stylesheet" href="${native_app_js_css_base_url}/static/css/ae_layout_system.css" />`);</script>
<script>document.write(`<link rel="stylesheet" href="${native_app_js_css_base_url}/static/ae_app_core_bundle.css" />`);</script>
<script>document.write(`<link rel="stylesheet" href="${native_app_js_css_base_url}/static/ae_app_mods_bundle.css" />`);</script>
<script>document.write(`<link rel="stylesheet" href="${native_app_js_css_base_url}/static/ae_app_mod_events_bundle.css" />`);</script>
<script>document.write(`<link rel="stylesheet" href="${native_app_js_css_base_url}/static/css/ae_temp.css" />`);</script>
<script>document.write(`<link rel="stylesheet" href="${native_app_js_css_base_url}/static/css/event.css" />`);</script>
<script>document.write(`<link rel="stylesheet" href="${native_app_js_css_base_url}/static/css/event_launcher.css" />`);</script>
<link rel="stylesheet" href="style.css">
<style>
</style>
<script>
// BEGIN: Environment constants set by server when rendering HTML
// Environment options: development_local, development_remote, production_local, production_remote,
// END: Environment constants set by server when rendering HTML
// BEGIN: Content constants set by server when rendering HTML
// const page_for_id = {"event": native_app_config.event_id, "event_device": native_app_config.event_device_id, "event_location": native_app_config.event_location_id, "event_session": native_app_config.event_session_id}; // Simple key value like object
const page_for = {'event_id': native_app_config.event_id, 'event_device_id': native_app_config.event_device_id, 'event_location_id': native_app_config.event_location_id, 'event_session_id': native_app_config.event_session_id}; // Simple key value like object
console.log('Page For:', page_for);
// END: Content constants set by server when rendering HTML
// BEGIN: Client constants set by server when rendering HTML
const client_account_jwt = {}; // Future use
const client_person_jwt = {}; // Future use
const client_user_jwt = {}; // Future use
let client_account_id = native_app_config.account_id;
// console.log(client_account_id);
// END: Client constants set by server when rendering HTML
// BEGIN: Other constants set by server when rendering HTML
// END: Other constants set by server when rendering HTML
document.aether.cfg = {};
document.aether.cfg.app = { "name": "One Sky IT's Aether Native App DEV", "version": "3.5 DEV", "email": null, "env": null, "mode": "native", "local_file_cache_path": native_app_config.local_file_cache_path, "host_file_temp_path": native_app_config.host_file_temp_path };
document.aether.cfg.api = { "protocol": native_app_config.api_protocol, "server": native_app_config.api_server, "port": native_app_config.api_port, "path": native_app_config.api_path, "secret_key": native_app_config.api_secret_key, "base_url": null, "temporary_token": {"token": null, "expire_on": null}, "protocol_backup": native_app_config.api_protocol_backup, "server_backup": native_app_config.api_server_backup, "port_backup": native_app_config.api_port_backup, "path_backup": native_app_config.api_path_backup, "secret_key_backup": native_app_config.api_secret_key_backup, "base_url_backup": null, "temporary_token_backup": {"token": null, "expire_on": null} };
document.aether.cfg.idb = { "name": native_app_config.idb_name };
document.aether.client = { "account_id": native_app_config.account_id, "site_id": native_app_config.site_id, "site_domain_id": native_app_config.site_domain_id, "person_id": null, "user_id": null, "order_cart_id_random": null, "super_check": false, "manager_check": false, "administrator_check": false, "support_check": false, "assistant_check": false, "trusted_check": false, "verified_check": false, "provisional_check": false, "public_check": false, "user_check": false, "logged_in_check": false, "person_check": false, "authenticated_check": false, "anonymous_check": true, "person_group": null, "user_group": null, "orders_closed_count": 0, "order_count": 0, "app_mode": "native", "administrator_passcode": "11500", "trusted_passcode": "19111", "authenticated_passcode": "20902" };
let page = {}
page['mode'] = null;
// page['page_for_id'] = { 'event': native_app_config.event_id, 'event_device': native_app_config.event_device_id, 'event_location': native_app_config.event_location_id, 'event_session': native_app_config.event_session_id }; // Simple key value like object;
page['page_for'] = { 'event_id': native_app_config.event_id, 'event_device_id': native_app_config.event_device_id, 'event_location_id': native_app_config.event_location_id, 'event_session_id': native_app_config.event_session_id }; // Simple key value like object;
page['current_url_root'] = null;
page['current_url_full_path'] = null;
page['data'] = null; // Pre-populate the page data from the rendering server (Electron app).
page['mode'] = null; // For future use or in other contexts
page['params'] = null; // For future use or in other contexts
document.aether.page = page;
let aether = JSON.stringify(document.aether);
let aether_cfg = JSON.stringify(document.aether.cfg);
let aether_client = JSON.stringify(document.aether.client);
let aether_page = JSON.stringify(document.aether.page);
localStorage.setItem('ae_cfg', aether_cfg);
sessionStorage.setItem('ae', aether);
sessionStorage.setItem('ae_client', aether_client);
sessionStorage.setItem('ae_page', aether_page);
/* ***** **** *** ** * ### * ** *** **** ***** */
let app_online = false;
//let app_use_cached_data = true;
window.addEventListener('online', native_app.currently_online);
window.addEventListener('offline', native_app.currently_offline);
/* ***** **** *** ** * ### * ** *** **** ***** */
const ae_bridge = {
example_var: 'Example Default Value',
get example() {
return this.example_var;
},
set example(new_value) {
this.example_var = new_value;
this.example_var_listener(new_value);
this.example_var_core_listener(new_value);
this.example_var_mods_listener(new_value);
},
example_var_listener: function (new_value) {},
registerNewListener: function (external_listener_function) {
this.example_var_listener = external_listener_function;
},
example_var_core_listener: function (new_value) {},
registerNewCoreListener: function (external_core_listener_function) {
this.example_var_core_listener = external_core_listener_function;
},
example_var_mods_listener: function (new_value) {},
registerNewModsListener: function (external_mods_listener_function) {
this.example_var_mods_listener = external_mods_listener_function;
},
// Monitor change in Access Type
access_type_var: 'anonymous',
get access_type() {
return this.access_type_var;
},
set access_type(new_value) {
this.access_type_var = new_value;
this.access_type_var_core_listener(new_value);
this.access_type_var_mods_listener(new_value);
},
access_type_var_core_listener: function (new_value) {},
register_core_access_type_listener: function (external_core_listener_function) {
this.access_type_var_core_listener = external_core_listener_function;
},
access_type_var_mods_listener: function (new_value) {},
register_mods_access_type_listener: function (external_mods_listener_function) {
this.access_type_var_mods_listener = external_mods_listener_function;
},
// Monitor change in AE Common
ae_com_var: 'anonymous',
get ae_com() {
return this.ae_com_var;
},
set ae_com(new_value) {
this.ae_com_var = new_value;
this.ae_com_var_core_listener(new_value);
this.ae_com_var_mods_listener(new_value);
},
ae_com_var_core_listener: function (new_value) {},
register_core_ae_com_listener: function (external_core_listener_function) {
this.ae_com_var_core_listener = external_core_listener_function;
},
ae_com_var_mods_listener: function (new_value) {},
register_mods_ae_com_listener: function (external_mods_listener_function) {
this.ae_com_var_mods_listener = external_mods_listener_function;
},
// Monitor change in Client
client_var: 'anonymous',
get client() {
return this.client_var;
},
set client(new_value) {
this.client_var = new_value;
this.client_var_core_listener(new_value);
this.client_var_mods_listener(new_value);
},
client_var_core_listener: function (new_value) {},
register_core_client_listener: function (external_core_listener_function) {
this.client_var_core_listener = external_core_listener_function;
},
client_var_mods_listener: function (new_value) {},
register_mods_client_listener: function (external_mods_listener_function) {
this.client_var_mods_listener = external_mods_listener_function;
},
};
/* ***** **** *** ** * ### * ** *** **** ***** */
let svelte_ae_app_core_bundle_js = document.createElement('script');
svelte_ae_app_core_bundle_js.setAttribute('src', `${native_app_js_css_base_url}/static/ae_app_core_bundle.js`);
document.head.appendChild(svelte_ae_app_core_bundle_js);
let svelte_ae_app_mods_bundle_js = document.createElement('script');
svelte_ae_app_mods_bundle_js.setAttribute('src', `${native_app_js_css_base_url}/static/ae_app_mods_bundle.js`);
document.head.appendChild(svelte_ae_app_mods_bundle_js);
let svelte_ae_app_mod_events_bundle_js = document.createElement('script');
svelte_ae_app_mod_events_bundle_js.setAttribute('src', `${native_app_js_css_base_url}/static/ae_app_mod_events_bundle.js`);
document.head.appendChild(svelte_ae_app_mod_events_bundle_js);
</script>
<!-- <script>document.write(`<script src="${native_app_js_css_base_url}/static/svelte/build/bundle.js" crossorigin />`);</script> -->
<!-- JavaScript (JS) end -->
</head>
<body id="Body-Container" class="body_container">
<section id="System-Nav-Menu">Site-Nav-Menu</section>
<div id="Site-Container">
<section id="Site-Header">Site-Header</section>
<nav id="Site-Nav-Menu">Site-Nav-Menu</nav>
<section id="Notifications">
<section id="System-Notifications">System-Notifications (and Site-Notifications)</section><!-- and what would be Site-Notifications-->
</section>
<section id="Main-Body" class="main_template_content svelte_target event_launcher_main Side-Main-Nav-Menu">
<section id="Main-Nav-Menu"></section>
<main id="Main-Content"></main>
</section>
<section id="Site-Set-Access-Type" class="svelte_target set_access_type"></section>
<section id="Site-Footer">Site-Footer</section>
</div>
<section id="Site-Modals"></section>
<section id="System-Footer">Site-Footer</section>
<section id="System-Debug">System-Debug</section>
</body>
<!-- JavaScript (JS) start -->
<!-- <script>const app = require('./js/app_v3');</script> -->
<!-- <script src="js/app_v3.js"></script> -->
<script>
/* ***** **** *** ** * ### * ** *** **** ***** */
/* ***** **** *** ** * ### * ** *** **** ***** */
</script>
<!-- JavaScript (JS) end -->
</html>

View File

@@ -1,279 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>One Sky IT's Aether App</title>
<link rel="shortcut icon" type="image/png" href="img/favicon.ico">
<!-- Cascading Style Sheets (CSS) start -->
<link rel="preconnect" href="https://fonts.gstatic.com">
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans:ital,wght@0,400;0,700;1,400;1,700&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Roboto+Mono:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;1,100;1,200;1,300;1,400;1,500;1,600;1,700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css" integrity="sha512-1ycn6IcaQQ40/MKBW2W4Rhis/DbILU74C1vSrLJxCq57o941Ym01SwNsOMqvEBFlcgUa6xLiPY/NS5R+E6ztJQ==" crossorigin="anonymous" referrerpolicy="no-referrer" />
<!-- Cascading Style Sheets (CSS) end -->
<!-- JavaScript (JS) start -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/axios/1.1.3/axios.min.js" integrity="sha512-0qU9M9jfqPw6FKkPafM3gy2CBAvUWnYVOfNPDYKVuRTel1PrciTj+a9P3loJB+j0QmN2Y0JYQmkBBS8W+mbezg==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="https://webrtc.github.io/adapter/adapter-latest.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.2.0/socket.io.js" integrity="sha512-WL6WGKMPBiM9PnHRYIn5YEtq0Z8XP4fkVb4qy7PP4vhmYQErJ/dySyXuFIMDf1eEYCXCrQrMJfkNwKc9gsjTjA==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/dayjs/1.10.7/dayjs.min.js" integrity="sha512-bwD3VD/j6ypSSnyjuaURidZksoVx3L1RPvTkleC48SbHCZsemT3VKMD39KknPnH728LLXVMTisESIBOAb5/W0Q==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/localforage/1.10.0/localforage.min.js" integrity="sha512-+BMamP0e7wn39JGL8nKAZ3yAQT2dL5oaXWr4ZYlTGkKOaoXM/Yj7c4oy50Ngz5yoUutAG17flueD4F6QpTlPng==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<!-- JavaScript (JS) end -->
<script>
const native_app = require('./js/aether_app_native_v4');
let native_app_config = native_app.load_init_config();
console.log('Native App Initial Config:', native_app_config);
// native_app_config = native_app.load_full_config(native_app_config);
// console.log('Native App Full Config:', native_app_config);
let native_app_js_css_base_url = native_app_config.native_app_js_css_base_url;
document.aether = {
name: 'Aether Native App (Electron and Svelte)'
};
</script>
<!-- One Sky IT Cascading Style Sheets (CSS) -->
<script>document.write(`<link rel="stylesheet" href="${native_app_js_css_base_url}/static/css/ae_css_variables.css" />`);</script>
<script>document.write(`<link rel="stylesheet" href="${native_app_js_css_base_url}/static/css/ae_utilities.css" />`);</script>
<script>document.write(`<link rel="stylesheet" href="${native_app_js_css_base_url}/static/css/ae_shared_components.css" />`);</script>
<script>document.write(`<link rel="stylesheet" href="${native_app_js_css_base_url}/static/css/ae_core_modules_components.css" />`);</script>
<script>document.write(`<link rel="stylesheet" href="${native_app_js_css_base_url}/static/css/ae_layout_base.css" />`);</script>
<script>document.write(`<link rel="stylesheet" href="${native_app_js_css_base_url}/static/css/ae_layout_system.css" />`);</script>
<script>document.write(`<link rel="stylesheet" href="${native_app_js_css_base_url}/static/ae_app_core_bundle.css" />`);</script>
<script>document.write(`<link rel="stylesheet" href="${native_app_js_css_base_url}/static/ae_app_mods_bundle.css" />`);</script>
<script>document.write(`<link rel="stylesheet" href="${native_app_js_css_base_url}/static/ae_app_mod_events_bundle.css" />`);</script>
<script>document.write(`<link rel="stylesheet" href="${native_app_js_css_base_url}/static/css/ae_temp.css" />`);</script>
<script>document.write(`<link rel="stylesheet" href="${native_app_js_css_base_url}/static/css/event.css" />`);</script>
<script>document.write(`<link rel="stylesheet" href="${native_app_js_css_base_url}/static/css/event_launcher.css" />`);</script>
<link rel="stylesheet" href="css/aether_app_native_v3.css">
<style>
</style>
<script>
// BEGIN: Environment constants set by server when rendering HTML
// Environment options: development_local, development_remote, production_local, production_remote,
// END: Environment constants set by server when rendering HTML
// BEGIN: Content constants set by server when rendering HTML
// const page_for_id = {"event": native_app_config.event_id, "event_device": native_app_config.event_device_id, "event_location": native_app_config.event_location_id, "event_session": native_app_config.event_session_id}; // Simple key value like object
const page_for = {'event_id': native_app_config.event_id, 'event_device_id': native_app_config.event_device_id, 'event_location_id': native_app_config.event_location_id, 'event_session_id': native_app_config.event_session_id}; // Simple key value like object
console.log('Page For:', page_for);
// END: Content constants set by server when rendering HTML
// BEGIN: Client constants set by server when rendering HTML
const client_account_jwt = {}; // Future use
const client_person_jwt = {}; // Future use
const client_user_jwt = {}; // Future use
let client_account_id = native_app_config.account_id;
console.log(client_account_id);
// END: Client constants set by server when rendering HTML
// BEGIN: Other constants set by server when rendering HTML
// END: Other constants set by server when rendering HTML
document.aether.cfg = {};
document.aether.cfg.app = { "name": "One Sky IT's Aether Native App DEV", "version": "3.5 DEV", "email": null, "env": null, "mode": "native", "local_file_cache_path": native_app_config.local_file_cache_path, "host_file_temp_path": native_app_config.host_file_temp_path };
document.aether.cfg.api = { "protocol": native_app_config.api_protocol, "server": native_app_config.api_server, "port": native_app_config.api_port, "path": native_app_config.api_path, "secret_key": native_app_config.api_secret_key, "base_url": null, "temporary_token": {"token": null, "expire_on": null}, "protocol_backup": native_app_config.api_protocol_backup, "server_backup": native_app_config.api_server_backup, "port_backup": native_app_config.api_port_backup, "path_backup": native_app_config.api_path_backup, "secret_key_backup": native_app_config.api_secret_key_backup, "base_url_backup": null, "temporary_token_backup": {"token": null, "expire_on": null} };
document.aether.cfg.idb = { "name": native_app_config.idb_name };
document.aether.client = { "account_id": native_app_config.account_id, "site_id": native_app_config.site_id, "site_domain_id": native_app_config.site_domain_id, "person_id": null, "user_id": null, "order_cart_id_random": null, "super_check": false, "manager_check": false, "administrator_check": false, "support_check": false, "assistant_check": false, "trusted_check": false, "verified_check": false, "provisional_check": false, "public_check": false, "user_check": false, "logged_in_check": false, "person_check": false, "authenticated_check": false, "anonymous_check": true, "person_group": null, "user_group": null, "orders_closed_count": 0, "order_count": 0, "app_mode": "native" };
let page = {}
page['mode'] = null;
// page['page_for_id'] = { 'event': native_app_config.event_id, 'event_device': native_app_config.event_device_id, 'event_location': native_app_config.event_location_id, 'event_session': native_app_config.event_session_id }; // Simple key value like object;
page['page_for'] = { 'event_id': native_app_config.event_id, 'event_device_id': native_app_config.event_device_id, 'event_location_id': native_app_config.event_location_id, 'event_session_id': native_app_config.event_session_id }; // Simple key value like object;
page['current_url_root'] = null;
page['current_url_full_path'] = null;
page['data'] = null; // Pre-populate the page data from the rendering server (Electron app).
page['mode'] = null; // For future use or in other contexts
page['params'] = null; // For future use or in other contexts
document.aether.page = page;
/* ***** **** *** ** * ### * ** *** **** ***** */
let app_online = false;
//let app_use_cached_data = true;
window.addEventListener('online', native_app.currently_online);
window.addEventListener('offline', native_app.currently_offline);
/* ***** **** *** ** * ### * ** *** **** ***** */
const ae_bridge = {
example_var: 'Example Default Value',
get example() {
return this.example_var;
},
set example(new_value) {
this.example_var = new_value;
this.example_var_listener(new_value);
this.example_var_core_listener(new_value);
this.example_var_mods_listener(new_value);
},
example_var_listener: function (new_value) {},
registerNewListener: function (external_listener_function) {
this.example_var_listener = external_listener_function;
},
example_var_core_listener: function (new_value) {},
registerNewCoreListener: function (external_core_listener_function) {
this.example_var_core_listener = external_core_listener_function;
},
example_var_mods_listener: function (new_value) {},
registerNewModsListener: function (external_mods_listener_function) {
this.example_var_mods_listener = external_mods_listener_function;
},
// Monitor change in Access Type
access_type_var: 'anonymous',
get access_type() {
return this.access_type_var;
},
set access_type(new_value) {
this.access_type_var = new_value;
this.access_type_var_core_listener(new_value);
this.access_type_var_mods_listener(new_value);
},
access_type_var_core_listener: function (new_value) {},
register_core_access_type_listener: function (external_core_listener_function) {
this.access_type_var_core_listener = external_core_listener_function;
},
access_type_var_mods_listener: function (new_value) {},
register_mods_access_type_listener: function (external_mods_listener_function) {
this.access_type_var_mods_listener = external_mods_listener_function;
},
// Monitor change in AE Common
ae_com_var: 'anonymous',
get ae_com() {
return this.ae_com_var;
},
set ae_com(new_value) {
this.ae_com_var = new_value;
this.ae_com_var_core_listener(new_value);
this.ae_com_var_mods_listener(new_value);
},
ae_com_var_core_listener: function (new_value) {},
register_core_ae_com_listener: function (external_core_listener_function) {
this.ae_com_var_core_listener = external_core_listener_function;
},
ae_com_var_mods_listener: function (new_value) {},
register_mods_ae_com_listener: function (external_mods_listener_function) {
this.ae_com_var_mods_listener = external_mods_listener_function;
},
// Monitor change in Client
client_var: 'anonymous',
get client() {
return this.client_var;
},
set client(new_value) {
this.client_var = new_value;
this.client_var_core_listener(new_value);
this.client_var_mods_listener(new_value);
},
client_var_core_listener: function (new_value) {},
register_core_client_listener: function (external_core_listener_function) {
this.client_var_core_listener = external_core_listener_function;
},
client_var_mods_listener: function (new_value) {},
register_mods_client_listener: function (external_mods_listener_function) {
this.client_var_mods_listener = external_mods_listener_function;
},
};
/* ***** **** *** ** * ### * ** *** **** ***** */
let svelte_ae_app_core_bundle_js = document.createElement('script');
svelte_ae_app_core_bundle_js.setAttribute('src', `${native_app_js_css_base_url}/static/ae_app_core_bundle.js`);
document.head.appendChild(svelte_ae_app_core_bundle_js);
let svelte_ae_app_mods_bundle_js = document.createElement('script');
svelte_ae_app_mods_bundle_js.setAttribute('src', `${native_app_js_css_base_url}/static/ae_app_mods_bundle.js`);
document.head.appendChild(svelte_ae_app_mods_bundle_js);
let svelte_ae_app_mod_events_bundle_js = document.createElement('script');
svelte_ae_app_mod_events_bundle_js.setAttribute('src', `${native_app_js_css_base_url}/static/ae_app_mod_events_bundle.js`);
document.head.appendChild(svelte_ae_app_mod_events_bundle_js);
</script>
<!-- <script>document.write(`<script src="${native_app_js_css_base_url}/static/svelte/build/bundle.js" crossorigin />`);</script> -->
<!-- JavaScript (JS) end -->
</head>
<body id="Body-Container" class="body_container">
<section id="System-Nav-Menu">Site-Nav-Menu</section>
<div id="Site-Container">
<section id="Site-Header">Site-Header</section>
<nav id="Site-Nav-Menu">Site-Nav-Menu</nav>
<section id="Notifications">
<section id="System-Notifications">System-Notifications (and Site-Notifications)</section><!-- and what would be Site-Notifications-->
</section>
<section id="Main-Body" class="main_template_content svelte_target event_launcher_main Side-Main-Nav-Menu">
<section id="Main-Nav-Menu"></section>
<main id="Main-Content"></main>
</section>
<section id="Site-Footer">Site-Footer</section>
</div>
<section id="Site-Modals"></section>
<section id="System-Footer">Site-Footer</section>
<section id="System-Debug">System-Debug</section>
</body>
<!-- JavaScript (JS) start -->
<!-- <script>const app = require('./js/app_v3');</script> -->
<!-- <script src="js/app_v3.js"></script> -->
<script>
/* ***** **** *** ** * ### * ** *** **** ***** */
/* ***** **** *** ** * ### * ** *** **** ***** */
</script>
<!-- JavaScript (JS) end -->
</html>

View File

@@ -1,632 +0,0 @@
'use strict';
const os = require('os');
const path = require('path');
const fs = require('fs');
const fs_promises = require('node:fs/promises');
const child_process = require('child_process');
const { ipcRenderer } = require('electron');
// import psList from 'ps-list';
// const ps_list = require('ps-list');
let home_directory = require('os').homedir();
console.log('Home: '+home_directory);
let tmp_directory = require('os').tmpdir();
console.log('Temporary: '+tmp_directory);
let config = null;
exports.load_config = function () {
console.log('*** Electron framework: load_config() ***');
let cwd = process.cwd();
console.log(`CWD: ${cwd}`);
try {
if (cwd == '/') {
cwd = home_directory;
}
console.log('Reading directory...');
let directory_list = fs_promises.readdir(cwd).then(function (read_dir_result) {
console.log('Got contents:');
for (let file of read_dir_result) {
console.log(file);
}
});
} catch (err) {
console.error(err);
}
// let home_directory = require('os').homedir();
// console.log('Home: '+home_directory);
// let tmp_directory = require('os').tmpdir();
// console.log('Temporary: '+tmp_directory);
// let config = null;
let config_directory = null;
// let default_config_path = path.join(process.cwd(),'config.json.default');
let default_config_path = 'config.json.default';
console.log(default_config_path);
let config_path = null;
// Set the config path for macOS or Linux
if (os.platform == 'darwin') {
config_directory = path.join(home_directory, 'Library/Application Support/OSIT');
console.log('macOS config directory: '+config_directory);
} else if (os.platform == 'linux') {
config_directory = path.join(home_directory, '.config/OSIT');
console.log('Linux config directory: '+config_directory);
}
// Look for the config file and copy the default if not found.
if (fs.existsSync(config_directory)) {
console.log('Config directory found: '+config_directory);
} else {
fs.mkdirSync(config_directory);
console.log('Config directory created: '+config_directory);
//default_config_path = path.join(process.cwd(),'config.json.default');
// config_path = path.join(config_directory, 'config.json');
// fs.copyFileSync(default_config_path, config_path);
// console.log('Default config file copied: '+config_directory);
}
config_path = path.join(config_directory, 'config.json');
// Attempt to open the config file. The preferred location is based on the OS's config directory.
if (fs.existsSync(config_path)) {
console.log(`Config file (config.json) found under ${config_directory}`);
} else if (!fs.existsSync(config_path) && fs.existsSync(default_config_path)) {
fs.copyFileSync(default_config_path, config_path);
console.log('Default config file copied: '+config_directory);
// config = JSON.parse(fs.readFileSync(config_path));
// console.log('Config file read.');
} else if (fs.existsSync(path.join(cwd, 'config.json'))) {
//fs.copyFileSync(default_config_path, config_path);
//console.log('Default config file copied: '+config_directory);
console.log(`Config file (config.json) not found under ${config_directory}. Using config in CWD. ${cwd}`);
config_path = path.join(cwd, 'config.json');
console.log(`Config file (config.json) not found under ${config_directory}. Using config in CWD. ${cwd}`);
let found_config_path = path.join(cwd, 'config.json');
fs.copyFileSync(found_config_path, config_path);
console.log(`Found config file copied: ${config_directory}`);
} else if (fs.existsSync(path.join(cwd, 'config.json.default'))) {
console.log(`Config file (config.json) not found under ${config_directory} or CWD. Using default config in CWD. ${cwd}`);
default_config_path = path.join(cwd, 'config.json.default');
fs.copyFileSync(default_config_path, config_path);
console.log(`Default config file copied: ${config_directory}`);
} else {
console.log('Can not find a config file.');
return false;
}
config = JSON.parse(fs.readFileSync(config_path));
console.log('Config file read.');
config.home_directory = home_directory; // From the OS platform
config.tmp_directory = tmp_directory; // From the OS platform
config.app_root_path = config.app_root_path.replace('[home]', home_directory);
config.app_root_path = config.app_root_path.replace('[tmp]', tmp_directory);
console.log(config.app_root_path);
config.local_file_cache_path = config.local_file_cache_path.replace('[home]', home_directory);
config.local_file_cache_path = config.local_file_cache_path.replace('[tmp]', tmp_directory);
console.log(config.local_file_cache_path);
// if (fs.existsSync(config.local_file_cache_path)) {
// } else {
// fs.mkdirSync(config.local_file_cache_path);
// console.log(`Host file cache directory created: ${config.local_file_cache_path}`);
// }
config.host_file_temp_path = config.host_file_temp_path.replace('[home]', home_directory);
config.host_file_temp_path = config.host_file_temp_path.replace('[tmp]', tmp_directory);
console.log(config.host_file_temp_path);
// if (fs.existsSync(config.host_file_temp_path)) {
// } else {
// fs.mkdirSync(config.host_file_temp_path);
// console.log(`Host file temp directory created: ${config.host_file_temp_path}`);
// }
let import_config_to_ipc_result = ipcRenderer.invoke('import_config', config).then((result) => {
console.log('IPC import config finished');
console.log(result);
return true;
})
//console.log(config);
return config;
}
// Check for local file
// Updated 2022-05-06
exports.check_local_file = async function ({local_file_path, filename}) {
console.log('*** Electron framework export: check_local_file() ***');
// console.log('Check for local file');
console.log(`Local File Path: ${local_file_path}; Filename: ${filename}`);
let full_local_file_path = path.join(local_file_path, filename);
console.log(full_local_file_path);
if (fs.existsSync(full_local_file_path)) {
console.log(`Local file exists: ${full_local_file_path}`);
return true;
} else {
return false;
}
}
// Check local hash file cache
// Used by Svelte Event Launcher
// NOTE: Trying to replace this with something directly in the Svelte app part. 2022-10-11
// Updated 2022-05-06
exports.check_hash_file_cache = async function ({local_file_cache_path, hash}) {
// console.log('*** Electron framework export: check_hash_file_cache() ***');
// console.log('Check local hash file cache');
console.log(`*** Electron framework export: check_hash_file_cache() *** Host File Cache Path: ${local_file_cache_path}; Hash: ${hash}`);
let hash_filename = `${hash}.file`;
let subdirectory = hash_filename.substring(0,2);
let subdirectory_path = path.join(local_file_cache_path, subdirectory);
if (fs.existsSync(subdirectory_path)) {
} else {
console.log(`Hashed file subdirectory not found in cache: ${subdirectory_path}`);
return false;
}
let hash_file_cache_path = path.join(subdirectory_path, hash_filename);
// console.log(hash_file_cache_path);
if (fs.existsSync(hash_file_cache_path)) {
// console.log(`Hashed file exists in cache: ${hash_file_cache_path}`);
return true;
} else {
console.log(`Hashed file not found in cache: ${hash_file_cache_path}`);
return false;
}
}
// Download hash file to cache
// Used by Svelte Event Launcher
// Updated 2022-05-06
exports.download_hash_file_to_cache = async function ({api_base_url, local_file_cache_path, event_file_id=null, hash=null}) {
// console.log('*** Electron framework export: download_hash_file_to_cache() ***');
// console.log('Download hash file to cache');
console.log(`*** Electron framework export: download_hash_file_to_cache() *** Base URL: ${api_base_url}; Host File Cache Path: ${local_file_cache_path}; Event File ID: ${event_file_id}; Hash: ${hash}`);
let endpoint = `/event/file/${event_file_id}/download`;
let hash_filename = `${hash}.file`;
let subdirectory = hash_filename.substring(0,2);
let subdirectory_path = path.join(local_file_cache_path, subdirectory);
if (fs.existsSync(subdirectory_path)) {
} else {
fs.mkdirSync(subdirectory_path);
console.log(`Subdirectory directory created: ${subdirectory_path}`);
}
let hash_file_cache_path = path.join(subdirectory_path, hash_filename);
if (fs.existsSync(hash_file_cache_path)) {
if (check_hash) {
const file_buffer = fs.readFileSync(hash_file_cache_path);
const file_hash_sha256 = crypto.createHash('sha256');
file_hash_sha256.update(file_buffer);
const file_hash_sha256_check = file_hash_sha256.digest('hex');S
if (file_hash_sha256_check == hash) {
console.log('File hash match', file_hash_sha256_check);
return true;
} else {
// This should only happen if the file is actively being downloaded or it is corrupt.
console.log('File hash does not match', file_hash_sha256_check);
return false;
}
}
}
// console.log(`!!!ABOUT TO CALL DOWNLOAD FILE HANDLER!!! exports.download_hash_file_to_cache(); Base URL: ${api_base_url}`);
let download_file_result = await ipcRenderer.invoke('download_file', api_base_url, endpoint, hash_file_cache_path).then((result) => {
if (result) {
console.log('IPC download file process finished successfully');
return true;
} else if (result == null) {
console.log('IPC Download Result (file not found?):', result);
return null;
} else {
console.log('IPC Download Result (file being downloaded or something went wrong):', result);
return false;
}
});
// console.log(`!!!DONE WITH DOWNLOAD FILE HANDLER!!! exports.download_hash_file_to_cache(); Base URL: ${api_base_url}`);
return download_file_result;
}
// Open cached hash file after copying to temp directory
// Used by Svelte Event Launcher
// NOTE: Trying to replace this with something directly in the Svelte app part. 2022-10-11
// Updated 2022-05-06
exports.open_hash_file_to_temp = async function ({local_file_cache_path, hash, host_file_temp_path, filename}) {
console.log('*** Electron framework export: open_hash_file_to_temp() ***');
// console.log('Open cached hash file after copying to temp directory');
console.log(`Host File Cache Path: ${local_file_cache_path}; Hash: ${hash}; Host File Temp Path: ${host_file_temp_path}; Filename: ${filename}`);
let subdirectory = hash.substring(0,2);
let subdirectory_path = path.join(local_file_cache_path, subdirectory);
if (fs.existsSync(subdirectory_path)) {
} else {
console.log(`Hashed file subdirectory not found in cache: ${subdirectory_path}`);
return false;
}
let open_hash_file_to_temp_result = await ipcRenderer.invoke('open_hash_file_to_temp', subdirectory_path, hash, host_file_temp_path, filename).then((result) => {
console.log('IPC open hash file to temp finished');
console.log(result);
return true;
})
// let result = await ipcRenderer.send('open_local_file', local_file_cache_path, hash, host_file_temp_path, filename);
// console.log(result);
console.log(open_hash_file_to_temp_result);
console.log('End: open_hash_file_to_temp()');
if (open_hash_file_to_temp_result) {
console.log('File opened successfully');
return true;
} else {
console.log('File was not opened successfully');
return false;
}
}
// Open local file
// Used by Svelte Event Launcher
// NOTE: Trying to replace this with something directly in the Svelte app part. 2022-10-11
// Updated 2022-03-10
exports.open_local_file = async function ({local_file_path, filename}) {
console.log('*** Electron framework export: open_local_file() ***');
// console.log('Open local file');
console.log(`Local File Path: ${local_file_path}; Filename: ${filename}`);
// let full_local_file_path = path.join(local_file_path, filename);
// console.log(full_local_file_path);
// if (fs.existsSync(full_local_file_path)) {
// console.log(`Local file exists: ${full_local_file_path}`);
// // return true;
// } else {
// return false;
// }
let open_local_file_result = await ipcRenderer.invoke('open_local_file', local_file_path, filename).then((result) => {
console.log('IPC open local file finished');
console.log(result);
return true;
})
console.log(open_local_file_result);
console.log('End: open_local_file()');
if (open_local_file_result) {
console.log('File opened successfully');
return true;
} else {
console.log('File was not opened successfully');
return false;
}
}
// // Check local file cache and download from server if needed.
// // Updated 2022-03-09
// // exports.check_file_cache = async function ({local_file_cache_path, event_file_id, hash}) {
// exports.check_file_cache = async function ({api_base_url, local_file_cache_path, event_file_id, hash}) {
// console.log('*** Electron framework export: check_file_cache() ***');
// // console.log('Check local file cache and download from server if needed.');
// console.log(`Host File Cache Path: ${local_file_cache_path}; Event File ID: ${event_file_id}; Hash: ${hash}`);
// // NOTE: event_file_id is the event_file.id_random or event_file.event_file_id_random
// let hash_filename = hash+'.file';
// let save_path = path.join(local_file_cache_path, hash_filename);
// console.log(save_path);
// if (fs.existsSync(save_path)) {
// console.log('Hashed file cache already exists: '+save_path);
// return true;
// } else {
// console.log('Hashed file not found in local cache. Downloading file: '+save_path);
// let endpoint = `/event/file/${event_file_id}/download`;
// let result = await ipcRenderer.send('download_file', api_base_url, endpoint, save_path); // Must download file using main node.js thread.
// console.log(result);
// return new Promise((resolve, reject) => {
// ipcRenderer.once('download_file_reply', function(event, response){
// console.log(response);
// return response;
// })
// resolve(true);
// });
// // await ipcRenderer.once('download_file_reply', function(event, response){
// // console.log(response);
// // return response;
// // });
// // result.then(function (response) {
// // console.log('Downloaded!!!???');
// // return true;
// // }).catch(function (error) {
// // console.log(error);
// // return false;
// // });
// // return result;
// // console.log(result);
// // if (result) {
// // return true;
// // } else {
// // return false;
// // }
// }
// }
// Check local file cache and download from server if needed. Must use IPC to Main to download file. Set a Promise to wait for download_file_reply.
// Updated 2022-03-09
async function check_file_cache({api_base_url, local_file_cache_path, event_file_id, hash}) {
console.log('*** Electron framework: check_file_cache() ***');
// console.log('Check local file cache and download from server if needed.');
console.log(`Host File Cache Path: ${local_file_cache_path}; Event File ID: ${event_file_id}; Hash: ${hash}`);
// NOTE: event_file_id is the event_file.id_random or event_file.event_file_id_random
let hash_filename = hash+'.file';
let save_path = path.join(local_file_cache_path, hash_filename);
console.log(save_path);
if (fs.existsSync(save_path)) {
console.log('Hashed file cache already exists: '+save_path);
return true;
} else {
console.log('Hashed file not found in local cache. Downloading file: '+save_path);
let endpoint = `/event/file/${event_file_id}/download`;
let result = await ipcRenderer.send('download_file', api_base_url, endpoint, save_path); // Must download file using main node.js thread.
console.log(result);
return new Promise((resolve, reject) => {
ipcRenderer.once('download_file_reply', function(event, response){
console.log(response);
return response;
})
resolve(true);
});
// await ipcRenderer.once('download_file_reply', function(event, response){
// console.log(response);
// return response;
// });
// result.then(function (response) {
// console.log('Downloaded!!!???');
// return true;
// }).catch(function (error) {
// console.log(error);
// return false;
// });
// return result;
// console.log(result);
// if (result) {
// return true;
// } else {
// return false;
// }
}
}
// IPC to Main: Open local file cache if available. Copy to temp directory with given filename first.
// Updated 2022-03-09
async function open_local_file({local_file_cache_path, hash, host_file_temp_path, filename}) {
console.log('*** Electron framework: open_local_file() ***');
// console.log('Open local file cache if available. Copy to temp directory with given filename first.');
console.log(`Host File Cache Path: ${local_file_cache_path}; Hash: ${hash}; Host File Temp Path: ${host_file_temp_path}; Filename: ${filename}`);
console.log(local_file_cache_path);
console.log(hash);
console.log(filename);
let result = await ipcRenderer.send('open_local_file', local_file_cache_path, hash, host_file_temp_path, filename);
console.log(result);
return true;
}
// No longer needed? Not referenced as of 2022-10-11
exports.check_file_cache_and_open_local_file = async function ({local_file_cache_path, event_file_id, hash, host_file_temp_path, filename}) {
console.log('*** Electron framework: check_file_cache_and_open_local_file() ***');
console.log('Checking the local file cache against the remote server and then opening the local file.');
let check_file_cache_result = check_file_cache({local_file_cache_path: local_file_cache_path, event_file_id: event_file_id, hash: hash});
console.log(check_file_cache_result);
if (check_file_cache_result) {
let open_local_file_result = open_local_file({local_file_cache_path: local_file_cache_path, hash: hash, host_file_temp_path: host_file_temp_path, filename: filename});
console.log(open_local_file_result);
return open_local_file_result;
}
ipcRenderer.once('download_file_reply', function(event, response){
console.log(response);
let open_local_file_result = open_local_file({local_file_cache_path: local_file_cache_path, hash: hash, host_file_temp_path: host_file_temp_path, filename: filename});
console.log(open_local_file_result);
return open_local_file_result;
})
}
// Kill processes
// Updated 2022-05-07
exports.kill_processes = async function ({process_name = null}) {
console.log('*** Electron framework export: kill_processes() ***');
console.log(process_name); // process_name or grep pattern
let cmd = '';
if (os.platform == 'darwin') {
// cmd = `osascript -e 'quit app "${process_name}" saving no'`;
cmd = `osascript -e 'quit application "${process_name}" saving no'`;
} else {
cmd = `pkill ${process_name}`;
}
child_process.exec(cmd, (err, stdout, stdin) => {
// if (err) throw err;
if (err) console.log(err);
console.log(stdout);
});
console.log(`Killed processes matching ${process_name}`);
if (os.platform == 'darwin') {
if (process_name == 'Parallels:Acrobat Reader') {
// Regular expression: (Parallels).*(Acrobat Reader)
// This will find any process with Parallels and Acrobat Reader in the name
cmd = `pkill -i -f '(Parallels).*(Acrobat Reader)'`;
child_process.exec(cmd, (err, stdout, stdin) => {
if (err) throw err;
console.log(stdout);
});
console.log('Killed Parallels Acrobat Reader process');
}
if (process_name == 'Parallels:PowerPoint') {
// Regular expression: (Parallels).*(PowerPoint)
// This will find any process with Parallels and PowerPoint in the name
cmd = `pkill -i -f '(Parallels).*(PowerPoint)'`;
child_process.exec(cmd, (err, stdout, stdin) => {
if (err) throw err;
console.log(stdout);
});
console.log('Killed Parallels PowerPoint process');
}
}
// let signal = 'SIGTERM'; // 'SIGTERM', 'SIGINT', 'SIGHUP'
// process.kill(pid, signal);
// process.kill(pid, 0); // Special case test if process exists
return true;
}
// Run raw osascript
// Updated 2022-05-07
exports.run_osascript = async function ({cmd=null, interactive=false, language=null, flags='h', program_file=null}) {
console.log('*** Electron framework export: run_osascript() ***');
console.log(cmd);
if (os.platform == 'darwin') {
} else {
console.log('Not available for this platform. macOS (darwin) only.');
return false;
}
let osascript_str = '';
if (Array.isArray(cmd)) {
console.log('List of cmd strings');
let cmds_str = '';
for (let i = 0; i < cmd.length; i++) {
cmds_str += `-e '${cmd[i]}'`;
}
osascript_str = `osascript ${cmds_str}`
} else if (typeof cmd === 'string') {
console.log('Single cmd string');
osascript_str = `osascript -e '${cmd}'`;
} else {
return false;
}
if (language) {
console.log(`Language: ${language}`);
osascript_str = `${osascript_str} -l ${language}`;
}
if (flags) {
console.log(`Flags: ${flags}`);
osascript_str = `${osascript_str} -s ${flags}`;
}
console.log(`OSA Script String: ${osascript_str}`);
child_process.exec(osascript_str, (err, stdout, stdin) => {
if (err) throw err;
console.log(stdout);
console.log(stdin);
});
console.log('Finished');
return true;
}
// Run raw command
// Updated 2022-05-07
exports.run_cmd = async function ({cmd=null}) {
console.log('*** Electron framework export: run_cmd() ***');
console.log(`Command String: ${cmd}`);
child_process.exec(cmd, (err, stdout, stdin) => {
if (err) throw err;
console.log(stdout);
console.log(stdin);
});
console.log('Finished');
return true;
}
// Run raw command
// Updated 2022-05-25
exports.get_device_info = async function () {
console.log('*** Electron framework export: get_device_info() ***');
let data = {};
data['arch'] = os.arch();
data['hostname'] = os.hostname();
data['cpus'] = os.cpus();
data['freemem'] = os.freemem();
data['totalmem'] = os.totalmem();
data['loadavg'] = os.loadavg();
data['networkInterfaces'] = os.networkInterfaces();
data['platform'] = os.platform();
data['release'] = os.release();
data['uptime'] = os.uptime();
data['version'] = os.version();
console.log(data);
return data;
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,28 +0,0 @@
body {
/* min-height: 100%;
height: 100%;
max-height: 100%; */
/* margin: .1em;
padding: .1em; */
}
section#Main-Body {
/* outline: solid thin red; */
/* min-height: 100%;
height: 100%;
max-height: 100%; */
}
section#Main-Nav-Menu {
/* min-height: 100%;
height: 100%;
max-height: 100%; */
}
section#Main-Content {
/* min-height: 100%;
height: 100%;
max-height: 100%; */
}

View File

@@ -1,279 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>One Sky IT's Aether App</title>
<link rel="shortcut icon" type="image/png" href="img/favicon.ico">
<!-- Cascading Style Sheets (CSS) start -->
<link rel="preconnect" href="https://fonts.gstatic.com">
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans:ital,wght@0,400;0,700;1,400;1,700&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Roboto+Mono:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;1,100;1,200;1,300;1,400;1,500;1,600;1,700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css" integrity="sha512-1ycn6IcaQQ40/MKBW2W4Rhis/DbILU74C1vSrLJxCq57o941Ym01SwNsOMqvEBFlcgUa6xLiPY/NS5R+E6ztJQ==" crossorigin="anonymous" referrerpolicy="no-referrer" />
<!-- Cascading Style Sheets (CSS) end -->
<!-- JavaScript (JS) start -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/axios/1.1.3/axios.min.js" integrity="sha512-0qU9M9jfqPw6FKkPafM3gy2CBAvUWnYVOfNPDYKVuRTel1PrciTj+a9P3loJB+j0QmN2Y0JYQmkBBS8W+mbezg==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="https://webrtc.github.io/adapter/adapter-latest.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.2.0/socket.io.js" integrity="sha512-WL6WGKMPBiM9PnHRYIn5YEtq0Z8XP4fkVb4qy7PP4vhmYQErJ/dySyXuFIMDf1eEYCXCrQrMJfkNwKc9gsjTjA==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/dayjs/1.10.7/dayjs.min.js" integrity="sha512-bwD3VD/j6ypSSnyjuaURidZksoVx3L1RPvTkleC48SbHCZsemT3VKMD39KknPnH728LLXVMTisESIBOAb5/W0Q==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/localforage/1.10.0/localforage.min.js" integrity="sha512-+BMamP0e7wn39JGL8nKAZ3yAQT2dL5oaXWr4ZYlTGkKOaoXM/Yj7c4oy50Ngz5yoUutAG17flueD4F6QpTlPng==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<!-- JavaScript (JS) end -->
<script>
const native_app = require('./js/aether_app_native_v4');
let native_app_config = native_app.load_init_config();
console.log('Native App Initial Config:', native_app_config);
// native_app_config = native_app.load_full_config(native_app_config);
// console.log('Native App Full Config:', native_app_config);
let native_app_js_css_base_url = native_app_config.native_app_js_css_base_url;
document.aether = {
name: 'Aether Native App (Electron and Svelte)'
};
</script>
<!-- One Sky IT Cascading Style Sheets (CSS) -->
<script>document.write(`<link rel="stylesheet" href="${native_app_js_css_base_url}/static/css/ae_css_variables.css" />`);</script>
<script>document.write(`<link rel="stylesheet" href="${native_app_js_css_base_url}/static/css/ae_utilities.css" />`);</script>
<script>document.write(`<link rel="stylesheet" href="${native_app_js_css_base_url}/static/css/ae_shared_components.css" />`);</script>
<script>document.write(`<link rel="stylesheet" href="${native_app_js_css_base_url}/static/css/ae_core_modules_components.css" />`);</script>
<script>document.write(`<link rel="stylesheet" href="${native_app_js_css_base_url}/static/css/ae_layout_base.css" />`);</script>
<script>document.write(`<link rel="stylesheet" href="${native_app_js_css_base_url}/static/css/ae_layout_system.css" />`);</script>
<script>document.write(`<link rel="stylesheet" href="${native_app_js_css_base_url}/static/ae_app_core_bundle.css" />`);</script>
<script>document.write(`<link rel="stylesheet" href="${native_app_js_css_base_url}/static/ae_app_mods_bundle.css" />`);</script>
<script>document.write(`<link rel="stylesheet" href="${native_app_js_css_base_url}/static/ae_app_mod_events_bundle.css" />`);</script>
<script>document.write(`<link rel="stylesheet" href="${native_app_js_css_base_url}/static/css/ae_temp.css" />`);</script>
<script>document.write(`<link rel="stylesheet" href="${native_app_js_css_base_url}/static/css/event.css" />`);</script>
<script>document.write(`<link rel="stylesheet" href="${native_app_js_css_base_url}/static/css/event_launcher.css" />`);</script>
<link rel="stylesheet" href="css/aether_app_native_v3.css">
<style>
</style>
<script>
// BEGIN: Environment constants set by server when rendering HTML
// Environment options: development_local, development_remote, production_local, production_remote,
// END: Environment constants set by server when rendering HTML
// BEGIN: Content constants set by server when rendering HTML
// const page_for_id = {"event": native_app_config.event_id, "event_device": native_app_config.event_device_id, "event_location": native_app_config.event_location_id, "event_session": native_app_config.event_session_id}; // Simple key value like object
const page_for = {'event_id': native_app_config.event_id, 'event_device_id': native_app_config.event_device_id, 'event_location_id': native_app_config.event_location_id, 'event_session_id': native_app_config.event_session_id}; // Simple key value like object
console.log('Page For:', page_for);
// END: Content constants set by server when rendering HTML
// BEGIN: Client constants set by server when rendering HTML
const client_account_jwt = {}; // Future use
const client_person_jwt = {}; // Future use
const client_user_jwt = {}; // Future use
let client_account_id = native_app_config.account_id;
console.log(client_account_id);
// END: Client constants set by server when rendering HTML
// BEGIN: Other constants set by server when rendering HTML
// END: Other constants set by server when rendering HTML
document.aether.cfg = {};
document.aether.cfg.app = { "name": "One Sky IT's Aether Native App DEV", "version": "3.5 DEV", "email": null, "env": null, "mode": "native", "local_file_cache_path": native_app_config.local_file_cache_path, "host_file_temp_path": native_app_config.host_file_temp_path };
document.aether.cfg.api = { "protocol": native_app_config.api_protocol, "server": native_app_config.api_server, "port": native_app_config.api_port, "path": native_app_config.api_path, "secret_key": native_app_config.api_secret_key, "base_url": null, "temporary_token": {"token": null, "expire_on": null}, "protocol_backup": native_app_config.api_protocol_backup, "server_backup": native_app_config.api_server_backup, "port_backup": native_app_config.api_port_backup, "path_backup": native_app_config.api_path_backup, "secret_key_backup": native_app_config.api_secret_key_backup, "base_url_backup": null, "temporary_token_backup": {"token": null, "expire_on": null} };
document.aether.cfg.idb = { "name": native_app_config.idb_name };
document.aether.client = { "account_id": native_app_config.account_id, "site_id": native_app_config.site_id, "site_domain_id": native_app_config.site_domain_id, "person_id": null, "user_id": null, "order_cart_id_random": null, "super_check": false, "manager_check": false, "administrator_check": false, "support_check": false, "assistant_check": false, "trusted_check": false, "verified_check": false, "provisional_check": false, "public_check": false, "user_check": false, "logged_in_check": false, "person_check": false, "authenticated_check": false, "anonymous_check": true, "person_group": null, "user_group": null, "orders_closed_count": 0, "order_count": 0, "app_mode": "native" };
let page = {}
page['mode'] = null;
// page['page_for_id'] = { 'event': native_app_config.event_id, 'event_device': native_app_config.event_device_id, 'event_location': native_app_config.event_location_id, 'event_session': native_app_config.event_session_id }; // Simple key value like object;
page['page_for'] = { 'event_id': native_app_config.event_id, 'event_device_id': native_app_config.event_device_id, 'event_location_id': native_app_config.event_location_id, 'event_session_id': native_app_config.event_session_id }; // Simple key value like object;
page['current_url_root'] = null;
page['current_url_full_path'] = null;
page['data'] = null; // Pre-populate the page data from the rendering server (Electron app).
page['mode'] = null; // For future use or in other contexts
page['params'] = null; // For future use or in other contexts
document.aether.page = page;
/* ***** **** *** ** * ### * ** *** **** ***** */
let app_online = false;
//let app_use_cached_data = true;
window.addEventListener('online', native_app.currently_online);
window.addEventListener('offline', native_app.currently_offline);
/* ***** **** *** ** * ### * ** *** **** ***** */
const ae_bridge = {
example_var: 'Example Default Value',
get example() {
return this.example_var;
},
set example(new_value) {
this.example_var = new_value;
this.example_var_listener(new_value);
this.example_var_core_listener(new_value);
this.example_var_mods_listener(new_value);
},
example_var_listener: function (new_value) {},
registerNewListener: function (external_listener_function) {
this.example_var_listener = external_listener_function;
},
example_var_core_listener: function (new_value) {},
registerNewCoreListener: function (external_core_listener_function) {
this.example_var_core_listener = external_core_listener_function;
},
example_var_mods_listener: function (new_value) {},
registerNewModsListener: function (external_mods_listener_function) {
this.example_var_mods_listener = external_mods_listener_function;
},
// Monitor change in Access Type
access_type_var: 'anonymous',
get access_type() {
return this.access_type_var;
},
set access_type(new_value) {
this.access_type_var = new_value;
this.access_type_var_core_listener(new_value);
this.access_type_var_mods_listener(new_value);
},
access_type_var_core_listener: function (new_value) {},
register_core_access_type_listener: function (external_core_listener_function) {
this.access_type_var_core_listener = external_core_listener_function;
},
access_type_var_mods_listener: function (new_value) {},
register_mods_access_type_listener: function (external_mods_listener_function) {
this.access_type_var_mods_listener = external_mods_listener_function;
},
// Monitor change in AE Common
ae_com_var: 'anonymous',
get ae_com() {
return this.ae_com_var;
},
set ae_com(new_value) {
this.ae_com_var = new_value;
this.ae_com_var_core_listener(new_value);
this.ae_com_var_mods_listener(new_value);
},
ae_com_var_core_listener: function (new_value) {},
register_core_ae_com_listener: function (external_core_listener_function) {
this.ae_com_var_core_listener = external_core_listener_function;
},
ae_com_var_mods_listener: function (new_value) {},
register_mods_ae_com_listener: function (external_mods_listener_function) {
this.ae_com_var_mods_listener = external_mods_listener_function;
},
// Monitor change in Client
client_var: 'anonymous',
get client() {
return this.client_var;
},
set client(new_value) {
this.client_var = new_value;
this.client_var_core_listener(new_value);
this.client_var_mods_listener(new_value);
},
client_var_core_listener: function (new_value) {},
register_core_client_listener: function (external_core_listener_function) {
this.client_var_core_listener = external_core_listener_function;
},
client_var_mods_listener: function (new_value) {},
register_mods_client_listener: function (external_mods_listener_function) {
this.client_var_mods_listener = external_mods_listener_function;
},
};
/* ***** **** *** ** * ### * ** *** **** ***** */
let svelte_ae_app_core_bundle_js = document.createElement('script');
svelte_ae_app_core_bundle_js.setAttribute('src', `${native_app_js_css_base_url}/static/ae_app_core_bundle.js`);
document.head.appendChild(svelte_ae_app_core_bundle_js);
let svelte_ae_app_mods_bundle_js = document.createElement('script');
svelte_ae_app_mods_bundle_js.setAttribute('src', `${native_app_js_css_base_url}/static/ae_app_mods_bundle.js`);
document.head.appendChild(svelte_ae_app_mods_bundle_js);
let svelte_ae_app_mod_events_bundle_js = document.createElement('script');
svelte_ae_app_mod_events_bundle_js.setAttribute('src', `${native_app_js_css_base_url}/static/ae_app_mod_events_bundle.js`);
document.head.appendChild(svelte_ae_app_mod_events_bundle_js);
</script>
<!-- <script>document.write(`<script src="${native_app_js_css_base_url}/static/svelte/build/bundle.js" crossorigin />`);</script> -->
<!-- JavaScript (JS) end -->
</head>
<body id="Body-Container" class="body_container">
<section id="System-Nav-Menu">Site-Nav-Menu</section>
<div id="Site-Container">
<section id="Site-Header">Site-Header</section>
<nav id="Site-Nav-Menu">Site-Nav-Menu</nav>
<section id="Notifications">
<section id="System-Notifications">System-Notifications (and Site-Notifications)</section><!-- and what would be Site-Notifications-->
</section>
<section id="Main-Body" class="main_template_content svelte_target event_launcher_main Side-Main-Nav-Menu">
<section id="Main-Nav-Menu"></section>
<main id="Main-Content"></main>
</section>
<section id="Site-Footer">Site-Footer</section>
</div>
<section id="Site-Modals"></section>
<section id="System-Footer">Site-Footer</section>
<section id="System-Debug">System-Debug</section>
</body>
<!-- JavaScript (JS) start -->
<!-- <script>const app = require('./js/app_v3');</script> -->
<!-- <script src="js/app_v3.js"></script> -->
<script>
/* ***** **** *** ** * ### * ** *** **** ***** */
/* ***** **** *** ** * ### * ** *** **** ***** */
</script>
<!-- JavaScript (JS) end -->
</html>

246
deploy/deploy.sh Executable file
View File

@@ -0,0 +1,246 @@
#!/usr/bin/env bash
# deploy.sh — Deploy Aether Native Launcher to onsite Mac laptops
#
# USAGE:
# ./deploy.sh <num> [num ...] Deploy to one or more laptops (e.g. 03 04 05)
# ./deploy.sh all Deploy to all laptops in devices.conf
# ./deploy.sh --seed-only <num> Update seed.json only — skip .app copy
# ./deploy.sh --seed-only all
# ./deploy.sh --build <num> [num ...] Build first (npm run package:mac), then deploy
# ./deploy.sh --build all
# ./deploy.sh --fix-accessibility <num> [num ...] Re-grant macOS Accessibility permission after .app update
# ./deploy.sh --fix-accessibility all (requires NOPASSWD sudo for sqlite3 — see README)
#
# REQUIRES:
# event.env — copy from event.env.example and fill in AETHER_API_KEY
# builds/ — pre-built, or use --build to build before deploying
#
# SSH keys must already be installed on each target (run ssh-copy-id once per laptop).
set -uo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
DEVICES_FILE="$SCRIPT_DIR/devices.conf"
EVENT_ENV="$SCRIPT_DIR/event.env"
SSH_USER="speaker ready"
BUILD_DIR="$SCRIPT_DIR/../builds"
# ── Argument parsing ──────────────────────────────────────────────────────────
SEED_ONLY=false
BUILD_FIRST=false
FIX_ACCESSIBILITY=false
TARGETS=()
# Bundle ID as embedded in Info.plist by electron-packager (no --app-bundle-id override)
BUNDLE_ID="com.electron.aetherlauncher"
usage() {
grep '^#' "$0" | grep -v '^#!/' | sed 's/^# \{0,1\}//'
exit 1
}
if [[ $# -eq 0 ]]; then usage; fi
while [[ $# -gt 0 ]]; do
case "$1" in
--seed-only) SEED_ONLY=true ;;
--build) BUILD_FIRST=true ;;
--fix-accessibility) FIX_ACCESSIBILITY=true ;;
--help|-h) usage ;;
all) TARGETS+=("all") ;;
*) TARGETS+=("$1") ;;
esac
shift
done
if [[ ${#TARGETS[@]} -eq 0 ]]; then
echo "ERROR: No targets specified."
usage
fi
# ── Load event config ─────────────────────────────────────────────────────────
if [[ ! -f "$EVENT_ENV" ]]; then
echo "ERROR: $EVENT_ENV not found."
echo " Copy event.env.example → event.env and fill in AETHER_API_KEY."
exit 1
fi
# shellcheck source=/dev/null
source "$EVENT_ENV"
: "${AETHER_API_KEY:?event.env must set AETHER_API_KEY}"
: "${PRIMARY_API_BASE_URL:?event.env must set PRIMARY_API_BASE_URL}"
: "${BACKUP_API_BASE_URL:?event.env must set BACKUP_API_BASE_URL}"
# Render onsite URL as JSON null or quoted string
if [[ -n "${ONSITE_API_BASE_URL:-}" ]]; then
ONSITE_JSON="\"$ONSITE_API_BASE_URL\""
else
ONSITE_JSON="null"
fi
# ── Device list helpers ───────────────────────────────────────────────────────
# Returns "ip device_id" for a laptop number, or empty if not found
lookup_device() {
local num="$1"
grep -v '^[[:space:]]*#' "$DEVICES_FILE" \
| grep -E "^[[:space:]]*${num}[[:space:]]" \
| awk '{print $2, $3}' \
| head -1
}
# Returns all laptop numbers from devices.conf (first column, non-comment lines)
all_device_nums() {
grep -v '^[[:space:]]*#' "$DEVICES_FILE" \
| grep -v '^[[:space:]]*$' \
| awk '{print $1}'
}
# ── Deploy one laptop ─────────────────────────────────────────────────────────
deploy_laptop() {
local num="$1"
local info
info=$(lookup_device "$num")
if [[ -z "$info" ]]; then
echo " ERROR: Laptop $num not found in devices.conf"
return 1
fi
local ip device_id
ip=$(echo "$info" | awk '{print $1}')
device_id=$(echo "$info" | awk '{print $2}')
echo ""
echo "══════════════════════════════════════════════"
echo " Laptop $num · $ip · $device_id"
echo "══════════════════════════════════════════════"
# ── Copy .app ──────────────────────────────────────────────────────────
if [[ "$SEED_ONLY" != "true" ]]; then
echo " Detecting architecture..."
local arch
arch=$(ssh "$SSH_USER@$ip" "uname -m" 2>/dev/null) || {
echo " ERROR: SSH failed for $ip — is the laptop on the network?"
return 1
}
local bundle
case "$arch" in
x86_64) bundle="$BUILD_DIR/aether_launcher-darwin-x64/aether_launcher.app" ;;
arm64) bundle="$BUILD_DIR/aether_launcher-darwin-arm64/aether_launcher.app" ;;
*)
echo " ERROR: Unknown arch '$arch' on $ip"
return 1
;;
esac
if [[ ! -d "$bundle" ]]; then
echo " ERROR: Build not found: $bundle"
echo " Run: npm run package:mac"
return 1
fi
echo " Arch: $arch → syncing $(basename "$bundle")..."
# rsync --delete syncs contents in-place without removing the top-level .app dir.
# This preserves the inode so macOS Aliases and Desktop shortcuts keep working.
rsync -a --delete -e ssh "$bundle/" "$SSH_USER@$ip:/Applications/aether_launcher.app/" || {
echo " ERROR: rsync failed."
return 1
}
echo " .app synced."
else
echo " (--seed-only: skipping .app copy)"
fi
# ── Write seed.json ────────────────────────────────────────────────────
echo " Writing seed.json..."
ssh "$SSH_USER@$ip" "cat > ~/seed.json" <<EOF
{
"event_device_id": "$device_id",
"aether_api_key": "$AETHER_API_KEY",
"primary_api_base_url": "$PRIMARY_API_BASE_URL",
"backup_api_base_url": "$BACKUP_API_BASE_URL",
"onsite_api_base_url": $ONSITE_JSON
}
EOF
# ── Accessibility permission ───────────────────────────────────────────
if [[ "$FIX_ACCESSIBILITY" == "true" ]]; then
echo " Resetting Accessibility permission (tccutil)..."
if ssh "$SSH_USER@$ip" "tccutil reset Accessibility $BUNDLE_ID" 2>/dev/null; then
echo " tccutil reset OK."
else
echo " WARNING: tccutil reset failed (non-fatal)."
fi
echo " Granting Accessibility via TCC database (requires NOPASSWD sudo)..."
# shellcheck disable=SC2016
TCC_SQL="INSERT OR REPLACE INTO access(service,client,client_type,auth_value,auth_reason,auth_version) VALUES('kTCCServiceAccessibility','$BUNDLE_ID',0,2,4,1);"
if ssh "$SSH_USER@$ip" "sudo sqlite3 '/Library/Application Support/com.apple.TCC/TCC.db' \"$TCC_SQL\"" 2>/dev/null; then
echo " ✓ Accessibility granted."
else
echo " WARNING: TCC grant failed — manual re-authorization required."
echo " See README: macOS Accessibility Permission."
fi
fi
# ── Verify ─────────────────────────────────────────────────────────────
echo " Verifying..."
ssh "$SSH_USER@$ip" "cat ~/seed.json"
if [[ "$SEED_ONLY" != "true" ]]; then
ssh "$SSH_USER@$ip" \
"test -d /Applications/aether_launcher.app && echo ' ✓ .app present' || echo ' ✗ .app NOT found'"
fi
echo " ✓ Laptop $num done."
return 0
}
# ── Build if requested ───────────────────────────────────────────────────────
if [[ "$BUILD_FIRST" == "true" ]]; then
echo "══════════════════════════════════════════════"
echo " Building: npm run package:mac"
echo "══════════════════════════════════════════════"
(cd "$SCRIPT_DIR/.." && npm run package:mac) || {
echo "ERROR: Build failed. Aborting deploy."
exit 1
}
echo ""
fi
# ── Expand "all" target ───────────────────────────────────────────────────────
EXPANDED_TARGETS=()
for t in "${TARGETS[@]}"; do
if [[ "$t" == "all" ]]; then
while IFS= read -r num; do
EXPANDED_TARGETS+=("$num")
done < <(all_device_nums)
else
EXPANDED_TARGETS+=("$t")
fi
done
# ── Run deploys ───────────────────────────────────────────────────────────────
FAILED=()
for num in "${EXPANDED_TARGETS[@]}"; do
if ! deploy_laptop "$num"; then
FAILED+=("$num")
fi
done
echo ""
echo "══════════════════════════════════════════════"
if [[ ${#FAILED[@]} -eq 0 ]]; then
echo " All done. ✓"
else
echo " FAILED: ${FAILED[*]}"
echo " Re-run with just those numbers to retry."
exit 1
fi

28
deploy/devices.conf Normal file
View File

@@ -0,0 +1,28 @@
# Aether Native Launcher — Device List
# Fields: laptop_num ip_address event_device_id [notes]
# Blank lines and lines starting with # are ignored.
#
# IP pattern: 192.168.32.1XX (XX = zero-padded laptop number)
# SSH user: "speaker ready" on all laptops
# Find/replace 192.168.32 for other venue network prefixes.
#
# num ip event_device_id notes
01 192.168.32.101 tFLL1fLQfnk
02 192.168.32.102 rpbfunVPEzw
03 192.168.192.203 1EPfPX8kfw8
04 192.168.192.204 zvgyLM5yieU
05 192.168.192.205 QOc046GoeSc
06 192.168.192.206 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
# x20 192.168.32.120 rwLYnKUNd1M old 04, spare/retired

15
deploy/event.env.example Normal file
View File

@@ -0,0 +1,15 @@
# event.env — Per-event deployment config for deploy.sh
# Copy this file to event.env and fill in the values before deploying.
# event.env is gitignored — never commit it (contains the API key).
#
# AETHER_API_KEY: shared across all laptops for this event deployment.
# Create in Aether admin (Core → Accounts or Events → Devices API key section)
# before the show. Delete after the show.
#
# ONSITE_API_BASE_URL: set to the local onsite API if running one (e.g.
# http://192.168.32.1/api). Leave blank to use null (cloud-only mode).
AETHER_API_KEY="your_api_key_here"
PRIMARY_API_BASE_URL="https://api.oneskyit.com"
BACKUP_API_BASE_URL="https://bak-api.oneskyit.com"
ONSITE_API_BASE_URL=""

65
dist/main/api_client.js vendored Normal file
View File

@@ -0,0 +1,65 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.fetchFullConfig = fetchFullConfig;
async function fetchFullConfig(seed) {
const apiUrls = [
seed.onsite_api_base_url,
seed.primary_api_base_url,
seed.backup_api_base_url
].filter(url => url !== null && url !== undefined);
let lastError = null;
for (const baseUrl of apiUrls) {
try {
console.log(`Bootstrap: Attempting connection to ${baseUrl}...`);
// --- STEP 1: Get Device Config ---
const deviceUrl = `${baseUrl}/v3/crud/event_device/${seed.event_device_id}`;
const deviceResponse = await fetch(deviceUrl, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'x-aether-api-key': seed.aether_api_key,
'x-no-account-id': 'bypass'
},
});
if (!deviceResponse.ok) {
throw new Error(`Device lookup failed (${deviceResponse.status})`);
}
const deviceResult = await deviceResponse.json();
const deviceData = deviceResult.data || deviceResult;
// Use 'app_base_url' as the FQDN for the site lookup
const fqdn = deviceData.app_base_url || 'native-demo.oneskyit.com';
// --- STEP 2: Get Site Context ---
const searchUrl = `${baseUrl}/v3/crud/site_domain/search?limit=1`;
const siteResponse = await fetch(searchUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-aether-api-key': seed.aether_api_key,
'x-account-id': deviceData.account_id_random || deviceData.account_id || ''
},
body: JSON.stringify({
and: [{ field: 'fqdn', op: 'eq', value: fqdn }]
})
});
if (!siteResponse.ok) {
throw new Error(`Site context lookup failed (${siteResponse.status})`);
}
const siteResult = await siteResponse.json();
const siteDomain = (siteResult.data && siteResult.data.length > 0) ? siteResult.data[0] : null;
console.log(`Bootstrap Success using ${baseUrl}`);
return {
...siteDomain,
native_device: deviceData,
aether_api_key: seed.aether_api_key // Include the key for frontend use
};
}
catch (error) {
console.warn(`Bootstrap failed for ${baseUrl}: `, error);
lastError = error;
continue; // Try next URL
}
}
console.error('Bootstrap Critical Failure: All API endpoints exhausted.', lastError);
return null;
}
//# sourceMappingURL=api_client.js.map

1
dist/main/api_client.js.map vendored Normal file
View File

@@ -0,0 +1 @@
{"version":3,"file":"api_client.js","sourceRoot":"","sources":["../../src/main/api_client.ts"],"names":[],"mappings":";;AAEA,0CAwEC;AAxEM,KAAK,UAAU,eAAe,CAAC,IAAgB;IACpD,MAAM,OAAO,GAAG;QACd,IAAI,CAAC,mBAAmB;QACxB,IAAI,CAAC,oBAAoB;QACzB,IAAI,CAAC,mBAAmB;KACzB,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,IAAI,IAAI,GAAG,KAAK,SAAS,CAAa,CAAC;IAE/D,IAAI,SAAS,GAAQ,IAAI,CAAC;IAE1B,KAAK,MAAM,OAAO,IAAI,OAAO,EAAE,CAAC;QAC9B,IAAI,CAAC;YACH,OAAO,CAAC,GAAG,CAAC,uCAAuC,OAAO,KAAK,CAAC,CAAC;YAEjE,oCAAoC;YACpC,MAAM,SAAS,GAAG,GAAG,OAAO,yBAAyB,IAAI,CAAC,eAAe,EAAE,CAAC;YAC5E,MAAM,cAAc,GAAG,MAAM,KAAK,CAAC,SAAS,EAAE;gBAC5C,MAAM,EAAE,KAAK;gBACb,OAAO,EAAE;oBACP,cAAc,EAAE,kBAAkB;oBAClC,kBAAkB,EAAE,IAAI,CAAC,cAAc;oBACvC,iBAAiB,EAAE,QAAQ;iBAC5B;aACF,CAAC,CAAC;YAEH,IAAI,CAAC,cAAc,CAAC,EAAE,EAAE,CAAC;gBACvB,MAAM,IAAI,KAAK,CAAC,yBAAyB,cAAc,CAAC,MAAM,GAAG,CAAC,CAAC;YACrE,CAAC;YAED,MAAM,YAAY,GAAG,MAAM,cAAc,CAAC,IAAI,EAAE,CAAC;YACjD,MAAM,UAAU,GAAG,YAAY,CAAC,IAAI,IAAI,YAAY,CAAC;YAErD,qDAAqD;YACrD,MAAM,IAAI,GAAG,UAAU,CAAC,YAAY,IAAI,0BAA0B,CAAC;YAEnE,mCAAmC;YACnC,MAAM,SAAS,GAAG,GAAG,OAAO,qCAAqC,CAAC;YAClE,MAAM,YAAY,GAAG,MAAM,KAAK,CAAC,SAAS,EAAE;gBAC1C,MAAM,EAAE,MAAM;gBACd,OAAO,EAAE;oBACP,cAAc,EAAE,kBAAkB;oBAClC,kBAAkB,EAAE,IAAI,CAAC,cAAc;oBACvC,cAAc,EAAE,UAAU,CAAC,iBAAiB,IAAI,UAAU,CAAC,UAAU,IAAI,EAAE;iBAC5E;gBACD,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC;oBACnB,GAAG,EAAE,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC;iBAChD,CAAC;aACH,CAAC,CAAC;YAEH,IAAI,CAAC,YAAY,CAAC,EAAE,EAAE,CAAC;gBACrB,MAAM,IAAI,KAAK,CAAC,+BAA+B,YAAY,CAAC,MAAM,GAAG,CAAC,CAAC;YACzE,CAAC;YAED,MAAM,UAAU,GAAG,MAAM,YAAY,CAAC,IAAI,EAAE,CAAC;YAC7C,MAAM,UAAU,GAAG,CAAC,UAAU,CAAC,IAAI,IAAI,UAAU,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;YAE/F,OAAO,CAAC,GAAG,CAAC,2BAA2B,OAAO,EAAE,CAAC,CAAC;YAElD,OAAO;gBACL,GAAG,UAAU;gBACb,aAAa,EAAE,UAAU;gBACzB,cAAc,EAAE,IAAI,CAAC,cAAc,CAAC,mCAAmC;aACxE,CAAC;QAEJ,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,CAAC,IAAI,CAAC,wBAAwB,OAAO,IAAI,EAAE,KAAK,CAAC,CAAC;YACzD,SAAS,GAAG,KAAK,CAAC;YAClB,SAAS,CAAC,eAAe;QAC3B,CAAC;IACH,CAAC;IAED,OAAO,CAAC,KAAK,CAAC,0DAA0D,EAAE,SAAS,CAAC,CAAC;IACrF,OAAO,IAAI,CAAC;AACd,CAAC"}

62
dist/main/config_loader.js vendored Normal file
View File

@@ -0,0 +1,62 @@
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.loadSeedConfig = loadSeedConfig;
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
const os = __importStar(require("os"));
async function loadSeedConfig() {
// For development, we look in the home directory
const configPath = path.join(os.homedir(), 'seed.json');
try {
if (!fs.existsSync(configPath)) {
console.log(`Seed config not found at: ${configPath}`);
return null;
}
const data = fs.readFileSync(configPath, 'utf-8');
const config = JSON.parse(data);
// Basic validation
if (!config.event_device_id) {
console.error('Invalid seed config: missing event_device_id');
return null;
}
return config;
}
catch (error) {
console.error('Error loading seed config:', error);
return null;
}
}
//# sourceMappingURL=config_loader.js.map

1
dist/main/config_loader.js.map vendored Normal file
View File

@@ -0,0 +1 @@
{"version":3,"file":"config_loader.js","sourceRoot":"","sources":["../../src/main/config_loader.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAKA,wCAwBC;AA7BD,uCAAyB;AACzB,2CAA6B;AAC7B,uCAAyB;AAGlB,KAAK,UAAU,cAAc;IAClC,iDAAiD;IACjD,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,EAAE,WAAW,CAAC,CAAC;IAExD,IAAI,CAAC;QACH,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC;YAC/B,OAAO,CAAC,GAAG,CAAC,6BAA6B,UAAU,EAAE,CAAC,CAAC;YACvD,OAAO,IAAI,CAAC;QACd,CAAC;QAED,MAAM,IAAI,GAAG,EAAE,CAAC,YAAY,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC;QAClD,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAe,CAAC;QAE9C,mBAAmB;QACnB,IAAI,CAAC,MAAM,CAAC,eAAe,EAAE,CAAC;YAC5B,OAAO,CAAC,KAAK,CAAC,8CAA8C,CAAC,CAAC;YAC9D,OAAO,IAAI,CAAC;QACd,CAAC;QAED,OAAO,MAAM,CAAC;IAChB,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO,CAAC,KAAK,CAAC,4BAA4B,EAAE,KAAK,CAAC,CAAC;QACnD,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC"}

217
dist/main/file_handlers.js vendored Normal file
View File

@@ -0,0 +1,217 @@
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.registerFileHandlers = registerFileHandlers;
const electron_1 = require("electron");
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
const os = __importStar(require("os"));
const crypto = __importStar(require("crypto"));
const child_process_1 = require("child_process");
const axios_1 = __importDefault(require("axios"));
const file_utils_1 = require("./file_utils");
let endpoints_in_progress = [];
function registerFileHandlers() {
// Flexible organization: [root]/[prefix_len-char-prefix]/[hash].file
function get_organized_hashed_path(root, hash, prefix_len = 2) {
const expanded_root = (0, file_utils_1.expandPath)(root);
const prefix = hash.substring(0, Math.max(1, Math.min(prefix_len, 8)));
const dir = path.join(expanded_root, prefix);
if (!fs.existsSync(dir))
fs.mkdirSync(dir, { recursive: true });
return path.join(dir, `${hash}.file`);
}
electron_1.ipcMain.handle('native:check-cache', async (event, { cache_root, hash, hash_prefix_length = 2, verify_hash = false }) => {
const full_path = get_organized_hashed_path(cache_root, hash, hash_prefix_length);
if (!fs.existsSync(full_path))
return false;
if (verify_hash) {
try {
const file_buffer = fs.readFileSync(full_path);
const actual_hash = crypto.createHash('sha256').update(file_buffer).digest('hex');
return actual_hash === hash;
}
catch (e) {
return false;
}
}
return true;
});
electron_1.ipcMain.handle('native:download-to-cache', async (event, { url, cache_root, hash, api_key, account_id, hash_prefix_length = 2 }) => {
const full_path = get_organized_hashed_path(cache_root, hash, hash_prefix_length);
const tmp_path = `${full_path}.tmp`;
if (endpoints_in_progress.includes(url))
return { success: true, status: 'in_progress' };
// 1. If final file exists, skip
if (fs.existsSync(full_path))
return { success: true, path: full_path, status: 'exists' };
// 2. Handle stale .tmp files (Legacy "Trust No One" pattern)
if (fs.existsSync(tmp_path)) {
const stats = fs.statSync(tmp_path);
const age_ms = Date.now() - stats.mtimeMs;
// If the tmp file is older than 5 minutes, assume previous download crashed and delete it
if (age_ms > 5 * 60 * 1000) {
console.log(`Native: Deleting stale temp file (${Math.round(age_ms / 1000)}s old)`);
fs.unlinkSync(tmp_path);
}
else {
return { success: true, status: 'in_progress', detail: 'fresh_tmp_exists' };
}
}
console.log(`Native: Hardened Download -> ${full_path}`);
try {
endpoints_in_progress.push(url);
const response = await (0, axios_1.default)({
method: 'get', url, responseType: 'stream',
headers: {
'x-aether-api-key': api_key,
'x-account-id': account_id || ''
}
});
const writer = fs.createWriteStream(tmp_path);
response.data.pipe(writer);
await new Promise((resolve, reject) => {
writer.on('finish', () => resolve());
writer.on('error', reject);
});
// 3. Verify Integrity before renaming (The "Trust No One" Check)
const file_buffer = fs.readFileSync(tmp_path);
const actual_hash = crypto.createHash('sha256').update(file_buffer).digest('hex');
if (actual_hash !== hash) {
console.error(`Native: Hash Mismatch! Expected ${hash}, got ${actual_hash}`);
fs.unlinkSync(tmp_path);
return { success: false, error: 'Integrity check failed: Hash mismatch' };
}
fs.renameSync(tmp_path, full_path);
console.log(`Native: Cache Integrity Verified. File moved to final destination.`);
return { success: true, path: full_path };
}
catch (error) {
if (fs.existsSync(tmp_path))
fs.unlinkSync(tmp_path);
return { success: false, error: error.message };
}
finally {
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, 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);
const target = path.join(expanded_temp, filename);
console.log(`Native: Launching from Cache -> ${filename}`);
if (!fs.existsSync(source)) {
return { success: false, error: `File not in cache: ${hash}` };
}
if (!fs.existsSync(expanded_temp))
fs.mkdirSync(expanded_temp, { recursive: true });
// 1. Copy the file to temp folder with original name
fs.copyFileSync(source, target);
// 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 (!native_template) {
return { success: false, error: 'No native template configured for this file' };
}
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() });
});
});
}
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 };
}
});
// Thin primitive: copy a cached file to the temp directory with its original filename,
// then return the resolved path. The caller (Svelte side) decides what to do next —
// run_osascript, run_cmd, open_local_file, etc.
//
// This is the preferred building block for custom launch flows. Use launch_from_cache
// 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 {
const source = get_organized_hashed_path(cache_root, hash, hash_prefix_length);
if (!fs.existsSync(source)) {
return { success: false, error: `File not in cache: ${hash}` };
}
const expanded_temp = (0, file_utils_1.expandPath)(temp_root);
const target = path.join(expanded_temp, filename);
if (!fs.existsSync(expanded_temp))
fs.mkdirSync(expanded_temp, { recursive: true });
fs.copyFileSync(source, target);
console.log(`Native: Copied from cache to temp -> ${target}`);
return { success: true, path: target };
}
catch (error) {
return { success: false, error: error.message };
}
});
}
//# sourceMappingURL=file_handlers.js.map

1
dist/main/file_handlers.js.map vendored Normal file

File diff suppressed because one or more lines are too long

53
dist/main/file_utils.js vendored Normal file
View File

@@ -0,0 +1,53 @@
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.expandPath = expandPath;
exports.getHashedPath = getHashedPath;
const os = __importStar(require("os"));
const path = __importStar(require("path"));
function expandPath(filePath) {
if (!filePath)
return filePath;
// Resolve all instances of [home] and [tmp] using global regex
return filePath
.replace(/\[home\]/g, os.homedir())
.replace(/\[tmp\]/g, os.tmpdir());
}
function getHashedPath(cacheRoot, hash) {
const expandedRoot = expandPath(cacheRoot);
const subdirectory = hash.substring(0, 2);
return path.join(expandedRoot, subdirectory, `${hash}.file`);
}
//# sourceMappingURL=file_utils.js.map

1
dist/main/file_utils.js.map vendored Normal file
View File

@@ -0,0 +1 @@
{"version":3,"file":"file_utils.js","sourceRoot":"","sources":["../../src/main/file_utils.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAGA,gCAMC;AAED,sCAIC;AAfD,uCAAyB;AACzB,2CAA6B;AAE7B,SAAgB,UAAU,CAAC,QAAgB;IACzC,IAAI,CAAC,QAAQ;QAAE,OAAO,QAAQ,CAAC;IAC/B,+DAA+D;IAC/D,OAAO,QAAQ;SACZ,OAAO,CAAC,WAAW,EAAE,EAAE,CAAC,OAAO,EAAE,CAAC;SAClC,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC,MAAM,EAAE,CAAC,CAAC;AACtC,CAAC;AAED,SAAgB,aAAa,CAAC,SAAiB,EAAE,IAAY;IAC3D,MAAM,YAAY,GAAG,UAAU,CAAC,SAAS,CAAC,CAAC;IAC3C,MAAM,YAAY,GAAG,IAAI,CAAC,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IAC1C,OAAO,IAAI,CAAC,IAAI,CAAC,YAAY,EAAE,YAAY,EAAE,GAAG,IAAI,OAAO,CAAC,CAAC;AAC/D,CAAC"}

120
dist/main/index.js vendored Normal file
View File

@@ -0,0 +1,120 @@
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
const electron_1 = require("electron");
const path = __importStar(require("path"));
const os = __importStar(require("os"));
const config_loader_1 = require("./config_loader");
const api_client_1 = require("./api_client");
const shell_handlers_1 = require("./shell_handlers");
const file_handlers_1 = require("./file_handlers");
const system_handlers_1 = require("./system_handlers");
let mainWindow = null;
let cachedSeed = null;
let cachedFullConfig = null;
async function createWindow() {
cachedSeed = await (0, config_loader_1.loadSeedConfig)();
if (cachedSeed) {
cachedFullConfig = await (0, api_client_1.fetchFullConfig)(cachedSeed);
}
mainWindow = new electron_1.BrowserWindow({
width: 1600,
height: 900,
title: 'OSIT Aether Launcher (Native)',
webPreferences: {
preload: path.join(__dirname, '../preload/index.js'),
contextIsolation: true,
nodeIntegration: false,
},
});
let targetUrl = 'http://demo.localhost:5173';
if (cachedFullConfig && cachedFullConfig.native_device) {
const device = cachedFullConfig.native_device;
const eventId = device.event_id_random || device.event_id;
const locationId = device.event_location_id_random || device.event_location_id || '';
// Use app_base_url from the device record (e.g. bgh.oneskyit.com).
// Fall back to localhost only if nothing is configured — never override a real domain.
const host = device.app_base_url || 'demo.localhost:5173';
// Use https for real domains; localhost dev URLs stay on http
const protocol = host.includes('localhost') ? 'http' : 'https';
targetUrl = `${protocol}://${host}/events/${eventId}/launcher/${locationId}`;
}
// Only open DevTools in development (not in a packaged .app build)
if (!electron_1.app.isPackaged)
mainWindow.webContents.openDevTools();
mainWindow.loadURL(targetUrl).catch(() => {
mainWindow?.loadURL('https://dev-demo.oneskyit.com/');
});
mainWindow.on('closed', () => { mainWindow = null; });
}
(0, shell_handlers_1.registerShellHandlers)();
(0, file_handlers_1.registerFileHandlers)();
(0, system_handlers_1.registerSystemHandlers)();
electron_1.app.on('ready', createWindow);
electron_1.app.on('window-all-closed', () => {
if (process.platform !== 'darwin')
electron_1.app.quit();
});
electron_1.app.on('activate', () => {
if (mainWindow === null)
createWindow();
});
electron_1.ipcMain.handle('get-seed-config', async () => cachedSeed || await (0, config_loader_1.loadSeedConfig)());
electron_1.ipcMain.handle('get-device-config', async () => cachedFullConfig);
electron_1.ipcMain.handle('get-jwt', async () => null);
electron_1.ipcMain.handle('get-device-info', async () => {
const interfaces = os.networkInterfaces();
const addresses = [];
for (const name of Object.keys(interfaces)) {
for (const net of interfaces[name]) {
if (net.family === 'IPv4' && !net.internal) {
addresses.push(net.address);
}
}
}
return {
platform: os.platform(),
release: os.release(),
arch: os.arch(),
hostname: os.hostname(),
cpus: os.cpus().length,
total_mem: os.totalmem(),
free_mem: os.freemem(),
ip_addresses: addresses,
home_directory: os.homedir(),
tmp_directory: os.tmpdir()
};
});
//# sourceMappingURL=index.js.map

1
dist/main/index.js.map vendored Normal file
View File

@@ -0,0 +1 @@
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/main/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,uCAAuD;AACvD,2CAA6B;AAC7B,uCAAyB;AACzB,mDAAiD;AACjD,6CAA+C;AAC/C,qDAAyD;AACzD,mDAAuD;AACvD,uDAA2D;AAG3D,IAAI,UAAU,GAAyB,IAAI,CAAC;AAC5C,IAAI,UAAU,GAAsB,IAAI,CAAC;AACzC,IAAI,gBAAgB,GAAQ,IAAI,CAAC;AAEjC,KAAK,UAAU,YAAY;IACzB,UAAU,GAAG,MAAM,IAAA,8BAAc,GAAE,CAAC;IACpC,IAAI,UAAU,EAAE,CAAC;QACf,gBAAgB,GAAG,MAAM,IAAA,4BAAe,EAAC,UAAU,CAAC,CAAC;IACvD,CAAC;IAED,UAAU,GAAG,IAAI,wBAAa,CAAC;QAC7B,KAAK,EAAE,IAAI;QACX,MAAM,EAAE,GAAG;QACX,KAAK,EAAE,+BAA+B;QACtC,cAAc,EAAE;YACd,OAAO,EAAE,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,qBAAqB,CAAC;YACpD,gBAAgB,EAAE,IAAI;YACtB,eAAe,EAAE,KAAK;SACvB;KACF,CAAC,CAAC;IAEH,IAAI,SAAS,GAAG,4BAA4B,CAAC;IAC7C,IAAI,gBAAgB,IAAI,gBAAgB,CAAC,aAAa,EAAE,CAAC;QACvD,MAAM,MAAM,GAAG,gBAAgB,CAAC,aAAa,CAAC;QAC9C,MAAM,OAAO,GAAG,MAAM,CAAC,eAAe,IAAI,MAAM,CAAC,QAAQ,CAAC;QAC1D,MAAM,UAAU,GAAG,MAAM,CAAC,wBAAwB,IAAI,MAAM,CAAC,iBAAiB,IAAI,EAAE,CAAC;QACrF,mEAAmE;QACnE,uFAAuF;QACvF,MAAM,IAAI,GAAG,MAAM,CAAC,YAAY,IAAI,qBAAqB,CAAC;QAC1D,8DAA8D;QAC9D,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC;QAC/D,SAAS,GAAG,GAAG,QAAQ,MAAM,IAAI,WAAW,OAAO,aAAa,UAAU,EAAE,CAAC;IAC/E,CAAC;IAED,mEAAmE;IACnE,IAAI,CAAC,cAAG,CAAC,UAAU;QAAE,UAAU,CAAC,WAAW,CAAC,YAAY,EAAE,CAAC;IAC3D,UAAU,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE;QACvC,UAAU,EAAE,OAAO,CAAC,gCAAgC,CAAC,CAAC;IACxD,CAAC,CAAC,CAAC;IAEH,UAAU,CAAC,EAAE,CAAC,QAAQ,EAAE,GAAG,EAAE,GAAG,UAAU,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;AACxD,CAAC;AAED,IAAA,sCAAqB,GAAE,CAAC;AACxB,IAAA,oCAAoB,GAAE,CAAC;AACvB,IAAA,wCAAsB,GAAE,CAAC;AAEzB,cAAG,CAAC,EAAE,CAAC,OAAO,EAAE,YAAY,CAAC,CAAC;AAE9B,cAAG,CAAC,EAAE,CAAC,mBAAmB,EAAE,GAAG,EAAE;IAC/B,IAAI,OAAO,CAAC,QAAQ,KAAK,QAAQ;QAAE,cAAG,CAAC,IAAI,EAAE,CAAC;AAChD,CAAC,CAAC,CAAC;AAEH,cAAG,CAAC,EAAE,CAAC,UAAU,EAAE,GAAG,EAAE;IACtB,IAAI,UAAU,KAAK,IAAI;QAAE,YAAY,EAAE,CAAC;AAC1C,CAAC,CAAC,CAAC;AAEH,kBAAO,CAAC,MAAM,CAAC,iBAAiB,EAAE,KAAK,IAAI,EAAE,CAAC,UAAU,IAAI,MAAM,IAAA,8BAAc,GAAE,CAAC,CAAC;AACpF,kBAAO,CAAC,MAAM,CAAC,mBAAmB,EAAE,KAAK,IAAI,EAAE,CAAC,gBAAgB,CAAC,CAAC;AAClE,kBAAO,CAAC,MAAM,CAAC,SAAS,EAAE,KAAK,IAAI,EAAE,CAAC,IAAI,CAAC,CAAC;AAE5C,kBAAO,CAAC,MAAM,CAAC,iBAAiB,EAAE,KAAK,IAAI,EAAE;IAC3C,MAAM,UAAU,GAAG,EAAE,CAAC,iBAAiB,EAAE,CAAC;IAC1C,MAAM,SAAS,GAAa,EAAE,CAAC;IAC/B,KAAK,MAAM,IAAI,IAAI,MAAM,CAAC,IAAI,CAAC,UAAU,CAAC,EAAE,CAAC;QAC3C,KAAK,MAAM,GAAG,IAAI,UAAU,CAAC,IAAI,CAAE,EAAE,CAAC;YACpC,IAAI,GAAG,CAAC,MAAM,KAAK,MAAM,IAAI,CAAC,GAAG,CAAC,QAAQ,EAAE,CAAC;gBAC3C,SAAS,CAAC,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;YAC9B,CAAC;QACH,CAAC;IACH,CAAC;IACD,OAAO;QACL,QAAQ,EAAE,EAAE,CAAC,QAAQ,EAAE;QACvB,OAAO,EAAE,EAAE,CAAC,OAAO,EAAE;QACrB,IAAI,EAAE,EAAE,CAAC,IAAI,EAAE;QACf,QAAQ,EAAE,EAAE,CAAC,QAAQ,EAAE;QACvB,IAAI,EAAE,EAAE,CAAC,IAAI,EAAE,CAAC,MAAM;QACtB,SAAS,EAAE,EAAE,CAAC,QAAQ,EAAE;QACxB,QAAQ,EAAE,EAAE,CAAC,OAAO,EAAE;QACtB,YAAY,EAAE,SAAS;QACvB,cAAc,EAAE,EAAE,CAAC,OAAO,EAAE;QAC5B,aAAa,EAAE,EAAE,CAAC,MAAM,EAAE;KAC3B,CAAC;AACJ,CAAC,CAAC,CAAC"}

290
dist/main/shell_handlers.js vendored Normal file
View File

@@ -0,0 +1,290 @@
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.registerShellHandlers = registerShellHandlers;
const electron_1 = require("electron");
const child_process_1 = require("child_process");
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
const os = __importStar(require("os"));
const file_utils_1 = require("./file_utils");
function registerShellHandlers() {
electron_1.ipcMain.handle('native:open-folder', async (event, folderPath) => {
const cleanPath = (0, file_utils_1.expandPath)(folderPath);
const error = await electron_1.shell.openPath(cleanPath);
return { success: !error, error };
});
electron_1.ipcMain.handle('native:run-cmd', async (event, { cmd, timeout = 30000 }) => {
const cleanCmd = (0, file_utils_1.expandPath)(cmd);
return new Promise((resolve) => {
(0, child_process_1.exec)(cleanCmd, { timeout }, (error, stdout, stderr) => {
resolve({ success: !error, stdout: stdout.trim(), stderr: stderr.trim(), error: error ? error.message : null });
});
});
});
electron_1.ipcMain.handle('native:run-cmd-sync', async (event, { cmd }) => {
const cleanCmd = (0, file_utils_1.expandPath)(cmd);
try {
const stdout = (0, child_process_1.execSync)(cleanCmd).toString();
return { success: true, stdout: stdout.trim() };
}
catch (error) {
return { success: false, error: error.message, stderr: error.stderr?.toString() };
}
});
electron_1.ipcMain.handle('native:run-osascript', async (event, script) => {
if (os.platform() !== 'darwin')
return { success: false, error: 'AppleScript is only available on macOS' };
// HARDENED: Write script to a temp .scpt file rather than passing inline via -e.
// The old -e approach (`osascript -e "..."`) has two fatal flaws:
// 1. It breaks on multi-line scripts.
// 2. It breaks on paths containing spaces or special characters (quotes, parens, etc.)
// Writing to a file sidesteps both — no shell escaping needed at all.
// The .scpt file is deleted immediately after execution (success or failure).
// Worst case on crash: a stale .scpt in /tmp, cleared on next OS reboot.
//
// LEGACY (removed): const cmd = `osascript -e "${script.replace(/"/g, '\\"')}"`;
const tmp_script_path = path.join(os.tmpdir(), `ae_osa_${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}"`, (error, stdout, stderr) => {
try {
fs.unlinkSync(tmp_script_path);
}
catch { }
resolve({ success: !error, stdout: stdout.trim(), stderr: stderr.trim(), error: error ? error.message : null });
});
});
});
electron_1.ipcMain.handle('native:kill-processes', async (event, { process_name_li = [] }) => {
console.log(`Native: Killing processes -> `, process_name_li);
const results = [];
for (const name of process_name_li) {
const cmd = os.platform() === 'win32'
? `taskkill /F /IM ${name} /T`
: `pkill -f ${name}`;
try {
(0, child_process_1.execSync)(cmd);
results.push({ name, success: true });
}
catch (e) {
results.push({ name, success: false, error: e.message });
}
}
return { success: true, results };
});
electron_1.ipcMain.handle('native:open-local-file-v2', async (event, filePath) => {
const cleanPath = (0, file_utils_1.expandPath)(filePath);
const error = await electron_1.shell.openPath(cleanPath);
return { success: !error, error };
});
electron_1.ipcMain.handle('native:launch-presentation', async (event, { path: rawPath, app: appType = 'default' }) => {
const cleanedPath = (0, file_utils_1.expandPath)(rawPath);
console.log(`Native: Launching Presentation -> ${cleanedPath} (App: ${appType})`);
if (os.platform() === 'linux') {
const cmd = `libreoffice --impress "${cleanedPath}"`;
return new Promise((resolve) => {
(0, child_process_1.exec)(cmd, (err, stdout, stderr) => {
if (err)
resolve({ success: false, error: err.message });
else
resolve({ success: true, stdout, stderr });
});
});
}
if (os.platform() === 'darwin') {
let script = '';
if (appType === 'keynote') {
script = `
tell application "Keynote"
activate
open (POSIX file "${cleanedPath}")
delay 1
start (front document)
end tell
`.trim();
}
else if (appType === 'powerpoint') {
script = `
tell application "Microsoft PowerPoint"
activate
open (POSIX file "${cleanedPath}")
delay 1
run slide show of active presentation
end tell
`.trim();
}
if (script) {
const tmp_script_path = path.join(os.tmpdir(), `ae_launch_${Date.now()}.scpt`);
return new Promise((resolve) => {
try {
fs.writeFileSync(tmp_script_path, script);
}
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 });
});
});
}
}
const error = await electron_1.shell.openPath(cleanedPath);
return { success: !error, error };
});
electron_1.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) => {
(0, child_process_1.exec)(`osascript -e "${script.replace(/"/g, '\\"')}"`, (error, stdout, stderr) => {
resolve({ success: !error, stdout: stdout.trim(), stderr: stderr.trim(), error: error ? error.message : null });
});
});
});
electron_1.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: {}
}
];
});
}
//# sourceMappingURL=shell_handlers.js.map

1
dist/main/shell_handlers.js.map vendored Normal file

File diff suppressed because one or more lines are too long

293
dist/main/system_handlers.js vendored Normal file
View File

@@ -0,0 +1,293 @@
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.registerSystemHandlers = registerSystemHandlers;
const electron_1 = require("electron");
const os = __importStar(require("os"));
const path = __importStar(require("path"));
const fs = __importStar(require("fs"));
const child_process_1 = require("child_process");
const axios_1 = __importDefault(require("axios"));
const file_utils_1 = require("./file_utils");
// Helper to execute shell commands
const runExec = (cmd) => {
return new Promise((resolve) => {
(0, child_process_1.exec)(cmd, (error, stdout, stderr) => {
resolve({
success: !error,
stdout: stdout.trim(),
stderr: stderr.trim(),
error: error ? error.message : undefined
});
});
});
};
let recordingProcess = null;
function registerSystemHandlers() {
// 1. Window Control
electron_1.ipcMain.handle('native:window-control', async (event, { action, value }) => {
const win = electron_1.BrowserWindow.fromWebContents(event.sender);
if (!win)
return { success: false, error: 'No window found' };
switch (action) {
case 'maximize':
win.maximize();
break;
case 'unmaximize':
win.unmaximize();
break;
case 'minimize':
win.minimize();
break;
case 'restore':
win.restore();
break;
case 'close':
win.close();
break;
case 'devtools':
if (value)
win.webContents.openDevTools();
else
win.webContents.closeDevTools();
break;
case 'kiosk':
win.setKiosk(!!value);
break;
case 'fullscreen':
win.setFullScreen(!!value);
break;
case 'reload':
win.reload();
break;
default: return { success: false, error: `Unknown action: ${action}` };
}
return { success: true };
});
// 2. Set Wallpaper
electron_1.ipcMain.handle('native:set-wallpaper', async (event, { path: imagePath }) => {
const cleanPath = (0, file_utils_1.expandPath)(imagePath);
if (!fs.existsSync(cleanPath))
return { success: false, error: 'Image file not found' };
if (os.platform() === 'darwin') {
const script = `tell application "System Events" to set picture of every desktop to "${cleanPath}"`;
return await runExec(`osascript -e '${script}'`);
}
else if (os.platform() === 'linux') {
// Gnome/Ubuntu default
return await runExec(`gsettings set org.gnome.desktop.background picture-uri "file://${cleanPath}"`);
}
return { success: false, error: 'Platform not supported' };
});
// 3. Power Control
electron_1.ipcMain.handle('native:power-control', async (event, { action }) => {
let cmd = '';
const isMac = os.platform() === 'darwin';
const isLinux = os.platform() === 'linux';
if (action === 'shutdown') {
if (isMac)
cmd = 'shutdown -h now'; // Requires sudo/admin usually
if (isLinux)
cmd = 'shutdown -h now';
}
else if (action === 'reboot') {
if (isMac)
cmd = 'shutdown -r now';
if (isLinux)
cmd = 'reboot';
}
else if (action === 'sleep') {
if (isMac)
cmd = 'pmset sleepnow';
if (isLinux)
cmd = 'systemctl suspend';
}
if (!cmd)
return { success: false, error: 'Action not supported' };
// NOTE: These commands often require root.
// For a kiosk, you might configure sudoers to allow this specific command without password.
return await runExec(cmd);
});
// 4. Open External (Browser)
electron_1.ipcMain.handle('native:open-external', async (event, { url, app: appName }) => {
if (appName === 'chrome') {
if (os.platform() === 'darwin') {
return await runExec(`open -a "Google Chrome" "${url}"`);
}
else if (os.platform() === 'linux') {
return await runExec(`google-chrome "${url}"`);
}
}
else if (appName === 'firefox') {
if (os.platform() === 'darwin') {
return await runExec(`open -a "Firefox" "${url}"`);
}
else if (os.platform() === 'linux') {
return await runExec(`firefox "${url}"`);
}
}
// Default system handler
await electron_1.shell.openExternal(url);
return { success: true };
});
// 5. Manage Recording (Aperture Wrapper)
electron_1.ipcMain.handle('native:manage-recording', async (event, { action, options }) => {
if (os.platform() !== 'darwin')
return { success: false, error: 'Recording only supported on macOS' };
// Path to bundled aperture binary
// In dev: ./resources/bin/aperture
// In prod: process.resourcesPath/bin/aperture
const binPath = electron_1.app.isPackaged
? path.join(process.resourcesPath, 'bin', 'aperture')
: path.join(__dirname, '../../resources/bin/aperture'); // Adjust based on structure
if (action === 'start') {
if (recordingProcess)
return { success: false, error: 'Recording already in progress' };
const { fps = 30, audioDeviceId, output } = options || {};
const cleanOutput = (0, file_utils_1.expandPath)(output || '~/tmp/recording.mp4');
const args = ['run', '--fps', fps, '--output', cleanOutput];
if (audioDeviceId)
args.push('--audio-device-id', audioDeviceId);
// Spawn process
// Note: aperture is a CLI tool. We might need 'aperture' node package or the binary.
// Assuming binary usage here.
try {
console.log(`Starting recording: ${binPath} ${args.join(' ')}`);
recordingProcess = (0, child_process_1.spawn)(binPath, args);
recordingProcess.on('error', (err) => {
console.error('Recording error:', err);
recordingProcess = null;
});
recordingProcess.on('exit', (code) => {
console.log(`Recording exited with code ${code}`);
recordingProcess = null;
});
return { success: true, pid: recordingProcess.pid };
}
catch (e) {
return { success: false, error: e.message };
}
}
else if (action === 'stop') {
if (!recordingProcess)
return { success: false, error: 'No recording in progress' };
recordingProcess.kill('SIGINT'); // Send interrupt to stop cleanly
recordingProcess = null;
return { success: true };
}
else if (action === 'status') {
return { success: true, isRecording: !!recordingProcess };
}
return { success: false, error: 'Unknown action' };
});
// 6. Set Display Layout (Displayplacer)
electron_1.ipcMain.handle('native:set-display-layout', async (event, { mode, configStr }) => {
if (os.platform() !== 'darwin')
return { success: false, error: 'Display control only supported on macOS' };
const binPath = electron_1.app.isPackaged
? path.join(process.resourcesPath, 'bin', 'displayplacer')
: path.join(__dirname, '../../resources/bin/displayplacer');
let cmd = '';
if (mode === 'mirror') {
// This usually requires querying current IDs, which is complex.
// If configStr is provided (output of 'displayplacer list'), use it.
if (configStr) {
cmd = `"${binPath}" ${configStr}`;
}
else {
return { success: false, error: 'Config string required for now' };
}
}
else if (mode === 'extend') {
if (configStr) {
cmd = `"${binPath}" ${configStr}`;
}
}
if (cmd) {
return await runExec(cmd);
}
return { success: false, error: 'Invalid mode or missing config' };
});
// 7. Update App
electron_1.ipcMain.handle('native:update-app', async (event, { source, url, path: localPath }) => {
// 1. Determine Source File
let updateFile = '';
const tempDir = os.tmpdir();
const destName = 'update_package.zip'; // Or .app, .AppImage
const destPath = path.join(tempDir, destName);
if (source === 'url' && url) {
// Download
try {
const response = await (0, axios_1.default)({
method: 'get',
url: url,
responseType: 'stream'
});
const writer = fs.createWriteStream(destPath);
response.data.pipe(writer);
await new Promise((resolve, reject) => {
writer.on('finish', () => resolve(true));
writer.on('error', reject);
});
updateFile = destPath;
}
catch (e) {
return { success: false, error: `Download failed: ${e.message}` };
}
}
else if (source === 'file' && localPath) {
const cleanLocal = (0, file_utils_1.expandPath)(localPath);
if (fs.existsSync(cleanLocal)) {
updateFile = cleanLocal;
}
else {
return { success: false, error: 'Local update file not found' };
}
}
if (!updateFile)
return { success: false, error: 'No update source provided' };
// 2. Install Logic (Stub)
// Real implementation depends on OS and packaging format.
// macOS: Mount DMG, copy .app to /Applications? Or Unzip .app?
// Linux: chmod +x AppImage and move?
console.log(`Ready to install update from: ${updateFile}`);
// For now, just return success so the UI knows we "downloaded" it.
return { success: true, message: 'Update downloaded/located. Installation logic requires packaging specifics.', downloadedPath: updateFile };
});
}
//# sourceMappingURL=system_handlers.js.map

1
dist/main/system_handlers.js.map vendored Normal file

File diff suppressed because one or more lines are too long

31
dist/preload/index.js vendored Normal file
View File

@@ -0,0 +1,31 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const electron_1 = require("electron");
electron_1.contextBridge.exposeInMainWorld('aetherNative', {
get_seed_config: () => electron_1.ipcRenderer.invoke('get-seed-config'),
get_device_config: () => electron_1.ipcRenderer.invoke('get-device-config'),
get_jwt: () => electron_1.ipcRenderer.invoke('get-jwt'),
get_device_info: () => electron_1.ipcRenderer.invoke('get-device-info'),
open_folder: (path) => electron_1.ipcRenderer.invoke('native:open-folder', path),
run_cmd: (args) => electron_1.ipcRenderer.invoke('native:run-cmd', args),
run_cmd_sync: (args) => electron_1.ipcRenderer.invoke('native:run-cmd-sync', args),
run_osascript: (script) => electron_1.ipcRenderer.invoke('native:run-osascript', script),
kill_processes: (args) => electron_1.ipcRenderer.invoke('native:kill-processes', args),
open_local_file_v2: (path) => electron_1.ipcRenderer.invoke('native:open-local-file-v2', path),
check_cache: (args) => electron_1.ipcRenderer.invoke('native:check-cache', args),
download_to_cache: (args) => electron_1.ipcRenderer.invoke('native:download-to-cache', args),
copy_from_cache_to_temp: (args) => electron_1.ipcRenderer.invoke('native:copy-from-cache-to-temp', args),
launch_from_cache: (args) => electron_1.ipcRenderer.invoke('native:launch-from-cache', args),
launch_presentation: (args) => electron_1.ipcRenderer.invoke('native:launch-presentation', args),
control_presentation: (args) => electron_1.ipcRenderer.invoke('native:control-presentation', args),
list_tools: () => electron_1.ipcRenderer.invoke('native:list-tools'),
// System Handlers (V5)
set_wallpaper: (args) => electron_1.ipcRenderer.invoke('native:set-wallpaper', args),
update_app: (args) => electron_1.ipcRenderer.invoke('native:update-app', args),
window_control: (args) => electron_1.ipcRenderer.invoke('native:window-control', args),
manage_recording: (args) => electron_1.ipcRenderer.invoke('native:manage-recording', args),
set_display_layout: (args) => electron_1.ipcRenderer.invoke('native:set-display-layout', args),
power_control: (args) => electron_1.ipcRenderer.invoke('native:power-control', args),
open_external: (args) => electron_1.ipcRenderer.invoke('native:open-external', args),
});
//# sourceMappingURL=index.js.map

1
dist/preload/index.js.map vendored Normal file
View File

@@ -0,0 +1 @@
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/preload/index.ts"],"names":[],"mappings":";;AAAA,uCAAsD;AAEtD,wBAAa,CAAC,iBAAiB,CAAC,cAAc,EAAE;IAC9C,eAAe,EAAE,GAAG,EAAE,CAAC,sBAAW,CAAC,MAAM,CAAC,iBAAiB,CAAC;IAC5D,iBAAiB,EAAE,GAAG,EAAE,CAAC,sBAAW,CAAC,MAAM,CAAC,mBAAmB,CAAC;IAChE,OAAO,EAAE,GAAG,EAAE,CAAC,sBAAW,CAAC,MAAM,CAAC,SAAS,CAAC;IAC5C,eAAe,EAAE,GAAG,EAAE,CAAC,sBAAW,CAAC,MAAM,CAAC,iBAAiB,CAAC;IAE5D,WAAW,EAAE,CAAC,IAAY,EAAE,EAAE,CAAC,sBAAW,CAAC,MAAM,CAAC,oBAAoB,EAAE,IAAI,CAAC;IAC7E,OAAO,EAAE,CAAC,IAAS,EAAE,EAAE,CAAC,sBAAW,CAAC,MAAM,CAAC,gBAAgB,EAAE,IAAI,CAAC;IAClE,YAAY,EAAE,CAAC,IAAS,EAAE,EAAE,CAAC,sBAAW,CAAC,MAAM,CAAC,qBAAqB,EAAE,IAAI,CAAC;IAC5E,aAAa,EAAE,CAAC,MAAc,EAAE,EAAE,CAAC,sBAAW,CAAC,MAAM,CAAC,sBAAsB,EAAE,MAAM,CAAC;IACrF,cAAc,EAAE,CAAC,IAAS,EAAE,EAAE,CAAC,sBAAW,CAAC,MAAM,CAAC,uBAAuB,EAAE,IAAI,CAAC;IAChF,kBAAkB,EAAE,CAAC,IAAY,EAAE,EAAE,CAAC,sBAAW,CAAC,MAAM,CAAC,2BAA2B,EAAE,IAAI,CAAC;IAE3F,WAAW,EAAE,CAAC,IAAS,EAAE,EAAE,CAAC,sBAAW,CAAC,MAAM,CAAC,oBAAoB,EAAE,IAAI,CAAC;IAC1E,iBAAiB,EAAE,CAAC,IAAS,EAAE,EAAE,CAAC,sBAAW,CAAC,MAAM,CAAC,0BAA0B,EAAE,IAAI,CAAC;IACtF,uBAAuB,EAAE,CAAC,IAAS,EAAE,EAAE,CAAC,sBAAW,CAAC,MAAM,CAAC,gCAAgC,EAAE,IAAI,CAAC;IAClG,iBAAiB,EAAE,CAAC,IAAS,EAAE,EAAE,CAAC,sBAAW,CAAC,MAAM,CAAC,0BAA0B,EAAE,IAAI,CAAC;IACtF,mBAAmB,EAAE,CAAC,IAAS,EAAE,EAAE,CAAC,sBAAW,CAAC,MAAM,CAAC,4BAA4B,EAAE,IAAI,CAAC;IAC1F,oBAAoB,EAAE,CAAC,IAAS,EAAE,EAAE,CAAC,sBAAW,CAAC,MAAM,CAAC,6BAA6B,EAAE,IAAI,CAAC;IAC5F,UAAU,EAAE,GAAG,EAAE,CAAC,sBAAW,CAAC,MAAM,CAAC,mBAAmB,CAAC;IAEzD,uBAAuB;IACvB,aAAa,EAAE,CAAC,IAAS,EAAE,EAAE,CAAC,sBAAW,CAAC,MAAM,CAAC,sBAAsB,EAAE,IAAI,CAAC;IAC9E,UAAU,EAAE,CAAC,IAAS,EAAE,EAAE,CAAC,sBAAW,CAAC,MAAM,CAAC,mBAAmB,EAAE,IAAI,CAAC;IACxE,cAAc,EAAE,CAAC,IAAS,EAAE,EAAE,CAAC,sBAAW,CAAC,MAAM,CAAC,uBAAuB,EAAE,IAAI,CAAC;IAChF,gBAAgB,EAAE,CAAC,IAAS,EAAE,EAAE,CAAC,sBAAW,CAAC,MAAM,CAAC,yBAAyB,EAAE,IAAI,CAAC;IACpF,kBAAkB,EAAE,CAAC,IAAS,EAAE,EAAE,CAAC,sBAAW,CAAC,MAAM,CAAC,2BAA2B,EAAE,IAAI,CAAC;IACxF,aAAa,EAAE,CAAC,IAAS,EAAE,EAAE,CAAC,sBAAW,CAAC,MAAM,CAAC,sBAAsB,EAAE,IAAI,CAAC;IAC9E,aAAa,EAAE,CAAC,IAAS,EAAE,EAAE,CAAC,sBAAW,CAAC,MAAM,CAAC,sBAAsB,EAAE,IAAI,CAAC;CAC/E,CAAC,CAAC"}

3
dist/shared/types.js vendored Normal file
View File

@@ -0,0 +1,3 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
//# sourceMappingURL=types.js.map

1
dist/shared/types.js.map vendored Normal file
View File

@@ -0,0 +1 @@
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/shared/types.ts"],"names":[],"mappings":""}

View File

@@ -0,0 +1,119 @@
# Native App Agent Task List
> Use this file to track steps for complex features or bug fixes.
> **Status:** Stable - ongoing development.
## Current Investigation
- This started as an API contract review for the native Electron bootstrap path and expanded into a packaging/runtime issue after the deploy step stopped producing bundles.
- 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.
- Removed a redundant `x-no-account-id` header from file download calls.
- Rewrote the device lookup smoke test so it validates the real two-step bootstrap path end to end.
- Upgraded Electron from 34.x to 42.0.1.
- Replaced deprecated `electron-packager` with `@electron/packager` 20.0.0.
- Added a `package:linux` smoke test path so packaging failures can be isolated from macOS-specific behavior.
- **Fixed packaging hang on Node 26:** `yauzl` 2.10.0 (used by `extract-zip` in `@electron/packager`) emits no `data` events on Node 26 streams, causing zip extraction to hang indefinitely. Fix: patched `@electron/packager/dist/unzip.js` to use `bsdtar` (libarchive) instead of `extract-zip`. `bsdtar` was chosen over `7z` because `7z` refuses macOS `.app` bundles with chained symlinks inside framework bundles. Patch is re-applied on every `npm install` via the `postinstall` script at `scripts/patch-packager-unzip.js`.
## Verified So Far
- `npm run dev` works once the Electron binary is present locally.
- Manual Electron cache extraction restored a runnable checkout on this machine.
- API validation confirmed the backend responds correctly for:
- `event_device/{id}` lookup
- `site_domain/search?limit=1` with the direct `SearchQuery` body
- The returned `site_domain.account_id` matches the device account context in the verified bootstrap flow.
- The SvelteKit frontend bootstrap path already follows the correct API contract and does not need the same fix.
- **`npm run package:linux` now produces `builds/aether_launcher-linux-x64/`** with a complete bundle (confirmed 2026-05-11).
- **`npm run package:mac` now produces `builds/aether_launcher-darwin-x64/` and `builds/aether_launcher-darwin-arm64/`** with `aether_launcher.app` inside each (confirmed 2026-05-11). Initial fix used `7z` but it refused chained symlinks inside macOS framework bundles; switched to `bsdtar` (libarchive) which handles both Linux and macOS zips correctly.
- `deploy/deploy.sh` output directory names (`aether_launcher-darwin-x64`, `aether_launcher-darwin-arm64`) match packager output — no script changes needed.
## Remaining Items
1. Test that the packaged Linux binary runs end-to-end against the dev API.
## Root Cause Summary (Packaging Hang)
- **Tool chain:** Node 26.1.0 + `@electron/packager` 20.0.0 + `extract-zip` 2.0.1 + `yauzl` 2.10.0
- **Symptom:** `npm run package:linux` exits 0 but produces no output. Debug log shows it starts extraction but never finishes.
- **Root cause:** `yauzl` opens a read stream for the first zip entry, but on Node 26, no `data` events are ever emitted on that stream. The `pipeline(readStream, writeStream)` call in `extract-zip` blocks forever.
- **Fix:** Replace the one-liner `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 macOS `.app` bundles with chained symlinks (e.g. `Electron Framework.framework/Libraries → Versions/Current/Libraries`). A `postinstall` npm script re-applies this patch after each `npm install`.
- **Build-time dependency:** `libarchive` (provides `bsdtar`) must be installed on the build host. On Arch: `pacman -S libarchive`; macOS: included in Xcode CLT or `brew install libarchive`; Ubuntu/Debian: `apt install libarchive-tools`.
## References
- Electron 42 release notes: https://www.electronjs.org/blog/electron-42-0
- Related Electron packaging discussion: https://github.com/aaddrick/claude-desktop-debian/pull/587
- Electron packaging/runtime change reference: https://github.com/electron/electron/pull/49328
- yauzl Node 26 stream issue: `yauzl` 2.10.0 uses legacy Node streams (streams1 style); Node 26 changed stream internal behavior so `openReadStream` returns a stream that never emits `data` without a proper pipeline consumer.
## Notes
- Was on Electron 34.
- The problem is not the backend API keys or the frontend site bootstrap flow.
- The packaging fix is a node_modules patch, not upstream. If `@electron/packager` or `extract-zip` releases a Node 26-compatible version, the `postinstall` script should be removed.
---
## Pending Feature: set_display_layout — displayplacer Per-Device Config
**Background (added 2026-05-12, from Svelte-side LaunchProfile work):**
The Svelte Events Launcher (`launcher_file_cont.svelte`) now resolves a `LaunchProfile` per file extension and calls `native.set_display_layout({ mode })` before opening a presentation. The underlying handler in `src/main/system_handlers.ts` uses a bundled `displayplacer` macOS binary. The wiring is complete on both ends — but it **silently no-ops on every device** because `displayplacer` requires a per-machine `configStr` (the output of `displayplacer list` on *that specific Mac*) to identify the exact display UUIDs and pixel positions. Without it, `displayplacer` cannot apply any layout.
**What's needed:**
1. **Capture `configStr` per room Mac.** On each presentation Mac, run:
```
displayplacer list
```
This prints the current display configuration. Copy the full output for both the "extend" and "mirror" layouts (they differ in which display is primary and how they're positioned).
2. **Store configs in the API.** The Svelte side reads device config from `event_device.data_json`. Store the captured strings there:
```json
{
"displayplacer_config_extend": "<full configStr for extended layout>",
"displayplacer_config_mirror": "<full configStr for mirrored layout>"
}
```
Admin UI to set these values already exists via the normal event_device edit flow. You can also set them directly via the V3 CRUD API (`PATCH /v3/crud/event_device/{id}/`).
3. ✅ **`set_display_layout` in `src/main/system_handlers.ts` already accepts `configStr`.** Handler signature is `{ mode, configStr }` and uses it correctly — no change needed.
4. ✅ **Electron relay (`src/preload/index.ts`) already forwards `configStr`** — args passed as-is. `AetherNativeBridge` type in `src/shared/types.ts` updated to include `set_display_layout` with correct signature (2026-05-12).
5. **Pass `configStr` from Svelte.** The Svelte call site in `launcher_file_cont.svelte` (Step 3 in `handle_open_file`) currently calls:
```ts
await native.set_display_layout({ mode: profile.display_mode }).catch(() => {});
```
It needs to be updated to thread `configStr` from `$ae_loc.native_device.data_json`:
```ts
const cfg_key = profile.display_mode === 'mirror'
? 'displayplacer_config_mirror'
: 'displayplacer_config_extend';
const configStr = ($ae_loc as any).native_device?.data_json?.[cfg_key] ?? null;
await native.set_display_layout({ mode: profile.display_mode, configStr }).catch(() => {});
```
**Contract already in place (Svelte side — no action needed):**
- `$ae_loc.native_device` is the `event_device` object loaded during native app bootstrap
- `data_json` is an open JSON field on that object
- `handle_open_file()` already calls `set_display_layout` at the right point — it just needs the configStr threaded through
**Resources:**
- displayplacer GitHub (usage + examples): https://github.com/jakehilborn/displayplacer
- `displayplacer list` — prints current layout as a re-runnable config string
- `displayplacer <configStr>` — applies layout; the configStr from `list` is what you pass back
- Current Electron handler: `src/main/system_handlers.ts` — find the `set_display_layout` IPC handler
- Svelte call site: `src/routes/events/[event_id]/(launcher)/launcher_file_cont.svelte` — Step 3 comment in `handle_open_file()`

View File

@@ -0,0 +1,567 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 50;
objects = {
/* Begin PBXBuildFile section */
3C278DA7225CF5C6004FFC53 /* masterkey.app in CopyFiles */ = {isa = PBXBuildFile; fileRef = 3C41339F2227E09B0041136E /* masterkey.app */; };
3C39A72F2267ABA90061367B /* WordMacAssistant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C39A72E2267ABA90061367B /* WordMacAssistant.swift */; };
3C39A7312267ACCF0061367B /* AcrobatWindowsAssistant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C39A7302267ACCF0061367B /* AcrobatWindowsAssistant.swift */; };
3C39A7332267ACF60061367B /* Windows Application Names in Resources */ = {isa = PBXBuildFile; fileRef = 3C39A7322267ACF60061367B /* Windows Application Names */; };
3C39A7352267AF040061367B /* WindowsVideoAssistant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C39A7342267AF040061367B /* WindowsVideoAssistant.swift */; };
3C39A739226A347A0061367B /* ExcelAssistant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C39A738226A347A0061367B /* ExcelAssistant.swift */; };
3C4133A32227E09B0041136E /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C4133A22227E09B0041136E /* AppDelegate.swift */; };
3C4133A52227E09F0041136E /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 3C4133A42227E09F0041136E /* Assets.xcassets */; };
3C4133A82227E09F0041136E /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 3C4133A62227E09F0041136E /* MainMenu.xib */; };
3C4133B42227E09F0041136E /* masterkeyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C4133B32227E09F0041136E /* masterkeyTests.swift */; };
3C4133C02227E1B70041136E /* PowerPointMacAssistant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C4133BF2227E1B70041136E /* PowerPointMacAssistant.swift */; };
3C4133C22227E1C90041136E /* PowerPointWindowsAssistant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C4133C12227E1C90041136E /* PowerPointWindowsAssistant.swift */; };
3C4133C42227E1D60041136E /* KeynoteAssistant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C4133C32227E1D60041136E /* KeynoteAssistant.swift */; };
3C4133C62227E1E50041136E /* LegacyKeynoteAssistant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C4133C52227E1E40041136E /* LegacyKeynoteAssistant.swift */; };
3C4133C82227E2010041136E /* AcrobatAssistant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C4133C72227E2010041136E /* AcrobatAssistant.swift */; };
3C4133CA2227E2960041136E /* ApplicationAssistant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C4133C92227E2960041136E /* ApplicationAssistant.swift */; };
3C88C06A225DADDD0048E01A /* VideoAssistant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C88C069225DADDD0048E01A /* VideoAssistant.swift */; };
3CE21AD62259B3EE00F666B9 /* Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CE21AD52259B3EE00F666B9 /* Utilities.swift */; };
3CE21AD92259B4A000F666B9 /* LegacyUtilities.m in Sources */ = {isa = PBXBuildFile; fileRef = 3CE21AD82259B4A000F666B9 /* LegacyUtilities.m */; };
3CE21ADC225A674800F666B9 /* LibreOfficeAssistant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CE21ADB225A674800F666B9 /* LibreOfficeAssistant.swift */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
3C4133B02227E09F0041136E /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 3C4133972227E09B0041136E /* Project object */;
proxyType = 1;
remoteGlobalIDString = 3C41339E2227E09B0041136E;
remoteInfo = masterkey;
};
/* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */
3CE0D699224155AE00E7FC1C /* CopyFiles */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = /Users/Ian/Desktop;
dstSubfolderSpec = 0;
files = (
3C278DA7225CF5C6004FFC53 /* masterkey.app in CopyFiles */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
3C39A72E2267ABA90061367B /* WordMacAssistant.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WordMacAssistant.swift; sourceTree = "<group>"; };
3C39A7302267ACCF0061367B /* AcrobatWindowsAssistant.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AcrobatWindowsAssistant.swift; sourceTree = "<group>"; };
3C39A7322267ACF60061367B /* Windows Application Names */ = {isa = PBXFileReference; lastKnownFileType = text; path = "Windows Application Names"; sourceTree = "<group>"; };
3C39A7342267AF040061367B /* WindowsVideoAssistant.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowsVideoAssistant.swift; sourceTree = "<group>"; };
3C39A738226A347A0061367B /* ExcelAssistant.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExcelAssistant.swift; sourceTree = "<group>"; };
3C41339F2227E09B0041136E /* masterkey.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = masterkey.app; sourceTree = BUILT_PRODUCTS_DIR; };
3C4133A22227E09B0041136E /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
3C4133A42227E09F0041136E /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
3C4133A72227E09F0041136E /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = "<group>"; };
3C4133A92227E09F0041136E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
3C4133AA2227E09F0041136E /* masterkey.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = masterkey.entitlements; sourceTree = "<group>"; };
3C4133AF2227E09F0041136E /* masterkeyTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = masterkeyTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
3C4133B32227E09F0041136E /* masterkeyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = masterkeyTests.swift; sourceTree = "<group>"; };
3C4133B52227E09F0041136E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
3C4133BF2227E1B70041136E /* PowerPointMacAssistant.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PowerPointMacAssistant.swift; sourceTree = "<group>"; };
3C4133C12227E1C90041136E /* PowerPointWindowsAssistant.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PowerPointWindowsAssistant.swift; sourceTree = "<group>"; };
3C4133C32227E1D60041136E /* KeynoteAssistant.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeynoteAssistant.swift; sourceTree = "<group>"; };
3C4133C52227E1E40041136E /* LegacyKeynoteAssistant.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacyKeynoteAssistant.swift; sourceTree = "<group>"; };
3C4133C72227E2010041136E /* AcrobatAssistant.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AcrobatAssistant.swift; sourceTree = "<group>"; };
3C4133C92227E2960041136E /* ApplicationAssistant.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationAssistant.swift; sourceTree = "<group>"; };
3C88C069225DADDD0048E01A /* VideoAssistant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VideoAssistant.swift; sourceTree = "<group>"; };
3CE21AD52259B3EE00F666B9 /* Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Utilities.swift; sourceTree = "<group>"; };
3CE21AD72259B4A000F666B9 /* masterkey-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "masterkey-Bridging-Header.h"; sourceTree = "<group>"; };
3CE21AD82259B4A000F666B9 /* LegacyUtilities.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = LegacyUtilities.m; sourceTree = "<group>"; };
3CE21ADA2259B5B900F666B9 /* LegacyUtilities.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = LegacyUtilities.h; sourceTree = "<group>"; };
3CE21ADB225A674800F666B9 /* LibreOfficeAssistant.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibreOfficeAssistant.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
3C41339C2227E09B0041136E /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
3C4133AC2227E09F0041136E /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
3C4133962227E09B0041136E = {
isa = PBXGroup;
children = (
3C4133A12227E09B0041136E /* masterkey */,
3C4133B22227E09F0041136E /* masterkeyTests */,
3C4133A02227E09B0041136E /* Products */,
);
sourceTree = "<group>";
};
3C4133A02227E09B0041136E /* Products */ = {
isa = PBXGroup;
children = (
3C41339F2227E09B0041136E /* masterkey.app */,
3C4133AF2227E09F0041136E /* masterkeyTests.xctest */,
);
name = Products;
sourceTree = "<group>";
};
3C4133A12227E09B0041136E /* masterkey */ = {
isa = PBXGroup;
children = (
3C4133BE2227E0B90041136E /* ApplicationAssistants */,
3C4133C92227E2960041136E /* ApplicationAssistant.swift */,
3C4133A22227E09B0041136E /* AppDelegate.swift */,
3C4133A42227E09F0041136E /* Assets.xcassets */,
3C4133A62227E09F0041136E /* MainMenu.xib */,
3C4133A92227E09F0041136E /* Info.plist */,
3C4133AA2227E09F0041136E /* masterkey.entitlements */,
3CE21AD52259B3EE00F666B9 /* Utilities.swift */,
3CE21ADA2259B5B900F666B9 /* LegacyUtilities.h */,
3CE21AD82259B4A000F666B9 /* LegacyUtilities.m */,
3CE21AD72259B4A000F666B9 /* masterkey-Bridging-Header.h */,
3C39A7322267ACF60061367B /* Windows Application Names */,
);
path = masterkey;
sourceTree = "<group>";
};
3C4133B22227E09F0041136E /* masterkeyTests */ = {
isa = PBXGroup;
children = (
3C4133B32227E09F0041136E /* masterkeyTests.swift */,
3C4133B52227E09F0041136E /* Info.plist */,
);
path = masterkeyTests;
sourceTree = "<group>";
};
3C4133BE2227E0B90041136E /* ApplicationAssistants */ = {
isa = PBXGroup;
children = (
3C4133BF2227E1B70041136E /* PowerPointMacAssistant.swift */,
3C4133C12227E1C90041136E /* PowerPointWindowsAssistant.swift */,
3C39A72E2267ABA90061367B /* WordMacAssistant.swift */,
3C4133C32227E1D60041136E /* KeynoteAssistant.swift */,
3C4133C52227E1E40041136E /* LegacyKeynoteAssistant.swift */,
3C4133C72227E2010041136E /* AcrobatAssistant.swift */,
3C39A7302267ACCF0061367B /* AcrobatWindowsAssistant.swift */,
3CE21ADB225A674800F666B9 /* LibreOfficeAssistant.swift */,
3C88C069225DADDD0048E01A /* VideoAssistant.swift */,
3C39A7342267AF040061367B /* WindowsVideoAssistant.swift */,
3C39A738226A347A0061367B /* ExcelAssistant.swift */,
);
path = ApplicationAssistants;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
3C41339E2227E09B0041136E /* masterkey */ = {
isa = PBXNativeTarget;
buildConfigurationList = 3C4133B82227E09F0041136E /* Build configuration list for PBXNativeTarget "masterkey" */;
buildPhases = (
3C41339B2227E09B0041136E /* Sources */,
3C41339C2227E09B0041136E /* Frameworks */,
3C41339D2227E09B0041136E /* Resources */,
3CE0D699224155AE00E7FC1C /* CopyFiles */,
);
buildRules = (
);
dependencies = (
);
name = masterkey;
productName = masterkey;
productReference = 3C41339F2227E09B0041136E /* masterkey.app */;
productType = "com.apple.product-type.application";
};
3C4133AE2227E09F0041136E /* masterkeyTests */ = {
isa = PBXNativeTarget;
buildConfigurationList = 3C4133BB2227E09F0041136E /* Build configuration list for PBXNativeTarget "masterkeyTests" */;
buildPhases = (
3C4133AB2227E09F0041136E /* Sources */,
3C4133AC2227E09F0041136E /* Frameworks */,
3C4133AD2227E09F0041136E /* Resources */,
);
buildRules = (
);
dependencies = (
3C4133B12227E09F0041136E /* PBXTargetDependency */,
);
name = masterkeyTests;
productName = masterkeyTests;
productReference = 3C4133AF2227E09F0041136E /* masterkeyTests.xctest */;
productType = "com.apple.product-type.bundle.unit-test";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
3C4133972227E09B0041136E /* Project object */ = {
isa = PBXProject;
attributes = {
LastSwiftUpdateCheck = 1010;
LastUpgradeCheck = 1010;
ORGANIZATIONNAME = "One Sky IT";
TargetAttributes = {
3C41339E2227E09B0041136E = {
CreatedOnToolsVersion = 10.1;
LastSwiftMigration = 1020;
SystemCapabilities = {
com.apple.Sandbox = {
enabled = 0;
};
};
};
3C4133AE2227E09F0041136E = {
CreatedOnToolsVersion = 10.1;
TestTargetID = 3C41339E2227E09B0041136E;
};
};
};
buildConfigurationList = 3C41339A2227E09B0041136E /* Build configuration list for PBXProject "masterkey" */;
compatibilityVersion = "Xcode 9.3";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 3C4133962227E09B0041136E;
productRefGroup = 3C4133A02227E09B0041136E /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
3C41339E2227E09B0041136E /* masterkey */,
3C4133AE2227E09F0041136E /* masterkeyTests */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
3C41339D2227E09B0041136E /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
3C4133A52227E09F0041136E /* Assets.xcassets in Resources */,
3C4133A82227E09F0041136E /* MainMenu.xib in Resources */,
3C39A7332267ACF60061367B /* Windows Application Names in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
3C4133AD2227E09F0041136E /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
3C41339B2227E09B0041136E /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
3C4133CA2227E2960041136E /* ApplicationAssistant.swift in Sources */,
3C4133C22227E1C90041136E /* PowerPointWindowsAssistant.swift in Sources */,
3C4133A32227E09B0041136E /* AppDelegate.swift in Sources */,
3C4133C42227E1D60041136E /* KeynoteAssistant.swift in Sources */,
3C39A739226A347A0061367B /* ExcelAssistant.swift in Sources */,
3C39A72F2267ABA90061367B /* WordMacAssistant.swift in Sources */,
3C4133C02227E1B70041136E /* PowerPointMacAssistant.swift in Sources */,
3CE21AD92259B4A000F666B9 /* LegacyUtilities.m in Sources */,
3C4133C62227E1E50041136E /* LegacyKeynoteAssistant.swift in Sources */,
3C39A7312267ACCF0061367B /* AcrobatWindowsAssistant.swift in Sources */,
3CE21AD62259B3EE00F666B9 /* Utilities.swift in Sources */,
3C39A7352267AF040061367B /* WindowsVideoAssistant.swift in Sources */,
3C4133C82227E2010041136E /* AcrobatAssistant.swift in Sources */,
3C88C06A225DADDD0048E01A /* VideoAssistant.swift in Sources */,
3CE21ADC225A674800F666B9 /* LibreOfficeAssistant.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
3C4133AB2227E09F0041136E /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
3C4133B42227E09F0041136E /* masterkeyTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
3C4133B12227E09F0041136E /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 3C41339E2227E09B0041136E /* masterkey */;
targetProxy = 3C4133B02227E09F0041136E /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin PBXVariantGroup section */
3C4133A62227E09F0041136E /* MainMenu.xib */ = {
isa = PBXVariantGroup;
children = (
3C4133A72227E09F0041136E /* Base */,
);
name = MainMenu.xib;
sourceTree = "<group>";
};
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
3C4133B62227E09F0041136E /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CODE_SIGN_IDENTITY = "-";
CODE_SIGN_INJECT_BASE_ENTITLEMENTS = NO;
CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 10.12;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = macosx;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
};
name = Debug;
};
3C4133B72227E09F0041136E /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CODE_SIGN_IDENTITY = "-";
CODE_SIGN_INJECT_BASE_ENTITLEMENTS = NO;
CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 10.12;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
SDKROOT = macosx;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
};
name = Release;
};
3C4133B92227E09F0041136E /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "-";
CODE_SIGN_STYLE = Manual;
COMBINE_HIDPI_IMAGES = YES;
DEVELOPMENT_TEAM = "";
INFOPLIST_FILE = masterkey/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 10.12;
PRODUCT_BUNDLE_IDENTIFIER = com.oneskyit.masterkey;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_OBJC_BRIDGING_HEADER = "masterkey/masterkey-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 4.2;
};
name = Debug;
};
3C4133BA2227E09F0041136E /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "-";
CODE_SIGN_STYLE = Manual;
COMBINE_HIDPI_IMAGES = YES;
DEVELOPMENT_TEAM = "";
INFOPLIST_FILE = masterkey/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 10.12;
PRODUCT_BUNDLE_IDENTIFIER = com.oneskyit.masterkey;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_OBJC_BRIDGING_HEADER = "masterkey/masterkey-Bridging-Header.h";
SWIFT_VERSION = 4.2;
};
name = Release;
};
3C4133BC2227E09F0041136E /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Manual;
COMBINE_HIDPI_IMAGES = YES;
DEVELOPMENT_TEAM = "";
INFOPLIST_FILE = masterkeyTests/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
"@loader_path/../Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.oneskyit.masterkeyTests;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_VERSION = 4.2;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/masterkey.app/Contents/MacOS/masterkey";
};
name = Debug;
};
3C4133BD2227E09F0041136E /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Manual;
COMBINE_HIDPI_IMAGES = YES;
DEVELOPMENT_TEAM = "";
INFOPLIST_FILE = masterkeyTests/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
"@loader_path/../Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.oneskyit.masterkeyTests;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_VERSION = 4.2;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/masterkey.app/Contents/MacOS/masterkey";
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
3C41339A2227E09B0041136E /* Build configuration list for PBXProject "masterkey" */ = {
isa = XCConfigurationList;
buildConfigurations = (
3C4133B62227E09F0041136E /* Debug */,
3C4133B72227E09F0041136E /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
3C4133B82227E09F0041136E /* Build configuration list for PBXNativeTarget "masterkey" */ = {
isa = XCConfigurationList;
buildConfigurations = (
3C4133B92227E09F0041136E /* Debug */,
3C4133BA2227E09F0041136E /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
3C4133BB2227E09F0041136E /* Build configuration list for PBXNativeTarget "masterkeyTests" */ = {
isa = XCConfigurationList;
buildConfigurations = (
3C4133BC2227E09F0041136E /* Debug */,
3C4133BD2227E09F0041136E /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = 3C4133972227E09B0041136E /* Project object */;
}

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:masterkey.xcodeproj">
</FileRef>
</Workspace>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>SchemeUserState</key>
<dict>
<key>masterkey.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>0</integer>
</dict>
</dict>
</dict>
</plist>

View File

@@ -0,0 +1,275 @@
//
// AppDelegate.swift
// masterkey
//
// Created by Ian Kohl on 2/28/19.
// Copyright © 2019 One Sky IT. All rights reserved.
//
//Open/Close
//quicktime - MP4
//Text
//Doc
import Cocoa
import OSAKit
enum ApplicationNameConstants: String {
case PowerPointMac = "Microsoft PowerPoint"
case PowerPointWin = "Microsoft Office PowerPoint"
case Keynote = "Keynote"
case LibreOffice = "LibreOffice"
case AdobeAcrobat = "Adobe Acrobat Reader DC"
case GoogleChrome = "Google Chrome"
case VLC = "VLC"
}
extension ApplicationNameConstants: CaseIterable {}
extension String {
func fileName() -> String {
return NSURL(fileURLWithPath: self).deletingPathExtension?.lastPathComponent ?? ""
}
func fileExtension() -> String {
return NSURL(fileURLWithPath: self).pathExtension ?? ""
}
}
@NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {
var assistant:ApplicationAssistant!
var openingFile = false
//No window
//@IBOutlet weak var window: NSWindow!
func applicationDidFinishLaunching(_ aNotification: Notification) {
if !openingFile {
NSLog("Not opening a file. Displaying setup dialog...")
if setupDialog() {
NSLog("Proceeding to trigger all security warnings...")
if triggerSecurity() {
NSLog("Presentation applications have been opened..")
}
}
}
NSLog("Terminating applicaiton...")
terminateApplication()
}
func terminateApplication() {
NSLog("Terminating...")
NSApplication.shared.terminate(self)
}
func applicationWillTerminate(_ aNotification: Notification) {
}
func application(_ sender: NSApplication, openFile filename: String) -> Bool {
NSLog("Version 2.0")
openingFile = true
//Used to open a group of files
//let first_filename = filenames[0];
//open a single file
let filename_extension = String(filename.fileExtension().lowercased());
NSLog("Filename Extension: \(filename_extension)")
var assistantFlag = false
//Args to pass to application - not currently used
//var args = ""
//once we are ready with a single presentation file (lowercase, valid ext) get into the switch
switch filename_extension {
case "pptx",
"ppt",
"pptmac",
"pptxmac":
NSLog("Mac PPT File...");
assistant = PowerPointMacAssistant();
case "pptxwin",
"pptwin":
NSLog("Windows PPT File...")
assistant = PowerPointWindowsAssistant();
case "pdf",
"pdfmac":
NSLog("PDF File...")
//args = "pagemode=FullScreen"
assistant = AcrobatAssistant();
case "pdfwin":
NSLog("PDF Windows File...")
assistant = AcrobatWindowsAssistant();
case "key":
NSLog("Keynote File...")
assistant = KeynoteAssistant();
case "odp":
NSLog("LibreOffice File...")
assistant = LibreOfficeAssistant();
case "mp4",
"mkv",
"mov",
"mpeg",
"avi",
"flv",
"ogg",
"mp3",
"ogv":
NSLog("Video (VLC) File...")
assistant = VideoAssistant();
case "wmv":
NSLog("Windows Video (VLC) File...")
assistant = WindowsVideoAssistant();
case "doc",
"docx":
NSLog("Microsoft Word Document...")
assistant = WordMacAssistant();
default:
NSLog("Generic File...")
assistantFlag = true
}
let legacyUtilities: LegacyUtilities = LegacyUtilities()
if assistantFlag {
NSLog("Is assistant...")
legacyUtilities.undoMirror()
//TODO: Window to mirror
NSWorkspace.shared.openFile(filename)
} else {
if assistant.shouldUnmirror {
legacyUtilities.undoMirror()
}
NSLog("Opening file...")
NSWorkspace.shared.openFile(filename, withApplication: assistant.applicationName)
NSLog("Sleeping...")
sleep(assistant!.delay)
NSLog("Activating application...")
let apps = NSRunningApplication.runningApplications(withBundleIdentifier: assistant.bundleID)
for app in apps as [NSRunningApplication] {
if app.bundleIdentifier == assistant.bundleID {
app.activate(options: .activateIgnoringOtherApps)
}
}
NSLog("Running Automation...")
if self.runAutomation() {
NSLog("Mirror check...")
if assistant.mirrored {
NSLog("Mirroring...")
legacyUtilities.setupMirror()
}
}
}
//Start recording code
var screens = 2
//TODO: get actual number of screens as mirroring/unmirroring should be done at this point
if assistant.mirrored {
screens = 1
}
let utils = Utilities()
let rec_result = utils.startRecording(filename: filename,num_screens: screens)
NSLog("Recording result...")
NSLog(rec_result)
//End recording code
// Return true for successfully opening the file.
return true
}
//TODO: Merge this and security as it is the same code
func runAutomation() -> Bool {
NSLog("Run automation...")
NSLog(assistant.scriptString)
let script = OSAScript.init(source: assistant.scriptString, language: OSALanguage.init(forName: "JavaScript"));
var compileError : NSDictionary?
script.compileAndReturnError(&compileError)
if let compileError = compileError {
NSLog("Compile Error...")
print(compileError);
return false;
}
var scriptError : NSDictionary?
let result = script.executeAndReturnError(&scriptError)
if let scriptError = scriptError {
NSLog("Print error...")
NSLog(scriptError.description)
}
else if let result = result?.stringValue {
NSLog("Result...")
print(result)
}
return true
}
func triggerSecurity() -> Bool {
NSLog("Trigger Security...")
ApplicationNameConstants.allCases.forEach {
let application_name = $0.rawValue
let application_security_script = """
var app = Application('\(application_name)')
delay(1)
app.activate()
"""
executeSecurityScript(script: application_security_script)
}
let final_security_script = """
var finder = Application('Finder')
finder.activate()
delay(1)
var se = Application('System Events')
se.keyCode(50)
"""
NSLog(final_security_script)
executeSecurityScript(script: final_security_script)
return true
}
func executeSecurityScript(script: String) {
let script = OSAScript.init(source: script, language: OSALanguage.init(forName: "JavaScript"));
var compileError : NSDictionary?
script.compileAndReturnError(&compileError)
if let compileError = compileError {
NSLog("Security Script Compile Error...")
print(compileError);
}
var scriptError : NSDictionary?
let result = script.executeAndReturnError(&scriptError)
if let scriptError = scriptError {
NSLog("Print security script error...")
NSLog(scriptError.description)
}
else if let result = result?.stringValue {
NSLog("Trigger Security Result...")
print(result)
}
}
func setupDialog() -> Bool {
let alert = NSAlert()
alert.messageText = "Are you setting up a new version of Master Key?"
alert.informativeText = "If you answer Yes, each applicaiton (PowerPoint, Keynote, etc.) will open so that security permissions can be granted for automation for each application. If you answer no, no action will be taken and the application will exit immediately."
alert.alertStyle = .warning
alert.addButton(withTitle: "Yes")
alert.addButton(withTitle: "No")
return alert.runModal() == .alertFirstButtonReturn
}
}

View File

@@ -0,0 +1,36 @@
//
// ApplicationAssistant.swift
// masterkey
//
// Created by Ian Kohl on 2/28/19.
// Copyright © 2019 One Sky IT. All rights reserved.
//
import Foundation
class ApplicationAssistant {
var mirrored: Bool
var shouldUnmirror: Bool
var delay: UInt32
var applicationName: String
var applicationPath: String
var scriptString: String
var bundleID: String
init() {
mirrored = false
shouldUnmirror = true
delay = 0
applicationName = ""
applicationPath = ""
scriptString = """
"""
bundleID = "xxxxxxxxxxxx"
}
}

View File

@@ -0,0 +1,80 @@
//
// PowerPointMacAssistant.swift
// masterkey
//
// Created by Ian Kohl on 2/28/19.
// Copyright © 2019 One Sky IT. All rights reserved.
//
import Foundation
class AcrobatAssistant: ApplicationAssistant {
// let mirrored = false
// let delay = 3.0
// let automationType = "JavaScript" //JSX or AppleScript, osascript might not care therefore remove this
//
// let applicationName = "Microsoft PowerPoint"
//
// let applicationPath = "/Applications/Microsoft PowerPoint.app"
//
// let scriptString =
// """
// delay .5\nactivate application \"Adobe Reader\"\ntell application \"System Events\"\ntell process \"Adobe Reader\"\nclick menu item \"Cascade\" of menu 1 of menu bar item \"Window\" of menu bar 1\ndelay 1\nclick menu item \"Full Screen Mode\" of menu 1 of menu bar item \"View\" of menu bar 1\nend tell\nend tell\n
//
// se.processes['Finder'].windows[0].toolbars[0].actions['AXShowMenu'].perform()
//
//
// var se = Application('System Events')
//
// seApp.keystroke('c', { using: 'command down' }) // Press C
// delay(0.2) // adjust the delay as needed
//
// var app = Application.currentApplication()
// app.includeStandardAdditions = true
//
// var powerpoint = Application('Microsoft PowerPoint')
// powerpoint.activate()
// delay(1)
// powerpoint.activate()
// """
// osascript -l JavaScript -e 'Application("iTunes").currentTrack.name()'
override init() {
super.init()
mirrored = true
shouldUnmirror = false
delay = 2
applicationName = "Adobe Acrobat Reader DC"
applicationPath = "/Applications/Adobe Acrobat Reader DC.app"
bundleID = "com.adobe.Reader"
// tell application "Adobe Acrobat Reader DC"
// open input -- args "pagemode=FullScreen"
// activate
// end tell
// var acrobat = Application('Adobe Acrobat Reader DC')
//
// acrobat.activate()
//
// var app = Application.currentApplication()
// app.includeStandardAdditions = true
//
// acrobat.activate()
// delay(1)
// acrobat.activate()
scriptString = """
delay(1)
var se = Application('System Events')
se.keyCode(37, { using: 'command down' })
"""
}
}

View File

@@ -0,0 +1,32 @@
//
// AcrobatWindowsAssistant.swift
// masterkey
//
// Created by Ian Kohl on 4/17/19.
// Copyright © 2019 One Sky IT. All rights reserved.
//
import Foundation
class AcrobatWindowsAssistant: ApplicationAssistant {
override init() {
super.init()
mirrored = true
shouldUnmirror = false
delay = 2
applicationName = "Acrobat Reader Windows"
applicationPath = "/Applications/Acrobat Reader Windows.app"
//ctrl + l
scriptString = """
delay(1)
var se = Application('System Events')
se.keyCode(108, { using: 'control down' })
"""
}
}

View File

@@ -0,0 +1,9 @@
//
// ExcelAssistant.swift
// masterkey
//
// Created by Ian Kohl on 4/19/19.
// Copyright © 2019 One Sky IT. All rights reserved.
//
import Foundation

View File

@@ -0,0 +1,42 @@
//
// PowerPointMacAssistant.swift
// masterkey
//
// Created by Ian Kohl on 2/28/19.
// Copyright © 2019 One Sky IT. All rights reserved.
//
import Foundation
class KeynoteAssistant: ApplicationAssistant {
override init() {
super.init()
mirrored = false
shouldUnmirror = true
delay = 2
applicationName = "Keynote"
applicationPath = "/Applications/Keynote.app"
bundleID = "com.apple.iWork.Keynote"
scriptString = """
delay(1)
var keynote = Application('Keynote')
var app = Application.currentApplication()
app.includeStandardAdditions = true
keynote.activate()
delay(1)
keynote.activate()
var doc = keynote.documents[0]
var slide = doc.slides[0]
doc.start()
"""
}
}

View File

@@ -0,0 +1,60 @@
//
// PowerPointMacAssistant.swift
// masterkey
//
// Created by Ian Kohl on 2/28/19.
// Copyright © 2019 One Sky IT. All rights reserved.
//
import Foundation
class LegacyKeynoteAssistant: ApplicationAssistant {
override init() {
super.init()
mirrored = false
shouldUnmirror = true
delay = 2
applicationName = "Keynote"
applicationPath = "/Applications/iWork '09/Keynote.app"
bundleID = "com.apple.iWork.Keynote"
// delay(1)
// var keynote = Application('Keynote')
//
// var app = Application.currentApplication()
// app.includeStandardAdditions = true
//
// keynote.activate()
// delay(1)
// keynote.activate()
//
// var doc = keynote.documents[0]
// var slide = doc.slides[0]
// doc.start()
//cmd + esc
scriptString = """
delay(1)
var keynote = Application('Keynote')
var app = Application.currentApplication()
app.includeStandardAdditions = true
keynote.activate()
delay(1)
keynote.activate()
var doc = keynote.documents[0]
var slide = doc.slides[0]
doc.start()
"""
}
}

View File

@@ -0,0 +1,43 @@
//
// LibreOfficeAssistant.swift
// masterkey
//
// Created by Ian Kohl on 4/7/19.
// Copyright © 2019 One Sky IT. All rights reserved.
//
import Foundation
class LibreOfficeAssistant: ApplicationAssistant {
override init() {
super.init()
mirrored = false
shouldUnmirror = true
delay = 2
applicationName = "LibreOffice"
applicationPath = "/Applications/LibreOffice.app"
bundleID = "org.libreoffice.script"
scriptString = """
delay(1)
var lo = Application('LibreOffice')
lo.activate()
var app = Application.currentApplication()
app.includeStandardAdditions = true
lo.activate()
delay(1)
lo.activate()
var se = Application('System Events')
se.keyCode(96)
"""
}
}

View File

@@ -0,0 +1,33 @@
//
// PowerPointMacAssistant.swift
// masterkey
//
// Created by Ian Kohl on 2/28/19.
// Copyright © 2019 One Sky IT. All rights reserved.
//
import Foundation
class PowerPointMacAssistant: ApplicationAssistant {
override init() {
super.init()
mirrored = false
shouldUnmirror = true
delay = 3
applicationName = "Microsoft PowerPoint"
applicationPath = "/Applications/Microsoft PowerPoint.app"
bundleID = "com.microsoft.Powerpoint"
scriptString = """
delay(1)
var se = Application('System Events')
se.keyCode(36, { using: 'command down' })
"""
}
}

View File

@@ -0,0 +1,42 @@
//
// PowerPointMacAssistant.swift
// masterkey
//
// Created by Ian Kohl on 2/28/19.
// Copyright © 2019 One Sky IT. All rights reserved.
//
import Foundation
class PowerPointWindowsAssistant: ApplicationAssistant {
override init() {
super.init()
mirrored = false
shouldUnmirror = true
delay = 3
applicationName = "Microsoft Office PowerPoint"
applicationPath = "/Applications/Microsoft Office PowerPoint.app"
//Not overriding bundleID and keeping the superclass xxxxxxxxxxxx bundleID. Automation code handles making sure Windows PPTs are active.
scriptString = """
var powerpoint = Application('Microsoft Office PowerPoint')
var app = Application.currentApplication()
app.includeStandardAdditions = true
delay(1)
powerpoint.activate()
var se = Application('System Events')
se.keyCode(96)
delay (2)
powerpoint.activate()
"""
}
}

View File

@@ -0,0 +1,48 @@
//
// PowerPointMacAssistant.swift
// masterkey
//
// Created by Ian Kohl on 2/28/19.
// Copyright © 2019 One Sky IT. All rights reserved.
//
import Foundation
class VideoAssistant: ApplicationAssistant {
override init() {
super.init()
mirrored = true
shouldUnmirror = false
delay = 2
applicationName = "VLC"
applicationPath = "/Applications/VLC.app"
bundleID = "org.videolan.vlc"
// tell application "Adobe Acrobat Reader DC"
// open input -- args "pagemode=FullScreen"
// activate
// end tell
// var acrobat = Application('Adobe Acrobat Reader DC')
//
// acrobat.activate()
//
// var app = Application.currentApplication()
// app.includeStandardAdditions = true
//
// acrobat.activate()
// delay(1)
// acrobat.activate()
scriptString = """
delay(1)
var se = Application('System Events')
se.keyCode(3, { using: 'command down' })
"""
}
}

View File

@@ -0,0 +1,32 @@
//
// WindowsVideoAssistant.swift
// masterkey
//
// Created by Ian Kohl on 4/17/19.
// Copyright © 2019 One Sky IT. All rights reserved.
//
import Foundation
class WindowsVideoAssistant: ApplicationAssistant {
override init() {
super.init()
mirrored = true
shouldUnmirror = false
delay = 2
applicationName = "VLC Windows"
applicationPath = "/Applications/VLC Windows.app"
//alt + enter
scriptString = """
delay(1)
var se = Application('System Events')
se.keyCode(36, { using: 'option down' })
"""
}
}

View File

@@ -0,0 +1,42 @@
//
// WordMacAssistant.swift
// masterkey
//
// Created by Ian Kohl on 4/17/19.
// Copyright © 2019 One Sky IT. All rights reserved.
//
import Foundation
class WordMacAssistant: ApplicationAssistant {
override init() {
super.init()
mirrored = true
shouldUnmirror = false
delay = 3
applicationName = "Microsoft Word"
applicationPath = "/Applications/Microsoft Word.app"
bundleID = "com.microsoft.Word"
scriptString = """
var word = Application('Microsoft Word')
var app = Application.currentApplication()
app.includeStandardAdditions = true
delay(1)
word.activate()
var se = Application('System Events')
se.keyCode(53, { using: 'command down' })
delay (2)
word.activate()
"""
}
}

View File

@@ -0,0 +1,58 @@
{
"images" : [
{
"idiom" : "mac",
"size" : "16x16",
"scale" : "1x"
},
{
"idiom" : "mac",
"size" : "16x16",
"scale" : "2x"
},
{
"idiom" : "mac",
"size" : "32x32",
"scale" : "1x"
},
{
"idiom" : "mac",
"size" : "32x32",
"scale" : "2x"
},
{
"idiom" : "mac",
"size" : "128x128",
"scale" : "1x"
},
{
"idiom" : "mac",
"size" : "128x128",
"scale" : "2x"
},
{
"idiom" : "mac",
"size" : "256x256",
"scale" : "1x"
},
{
"idiom" : "mac",
"size" : "256x256",
"scale" : "2x"
},
{
"idiom" : "mac",
"size" : "512x512",
"scale" : "1x"
},
{
"idiom" : "mac",
"size" : "512x512",
"scale" : "2x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

View File

@@ -0,0 +1,6 @@
{
"info" : {
"version" : 1,
"author" : "xcode"
}
}

View File

@@ -0,0 +1,78 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="14490.70" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<dependencies>
<deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="14490.70"/>
</dependencies>
<objects>
<customObject id="-2" userLabel="File's Owner" customClass="NSApplication">
<connections>
<outlet property="delegate" destination="Voe-Tx-rLC" id="GzC-gU-4Uq"/>
</connections>
</customObject>
<customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
<customObject id="-3" userLabel="Application" customClass="NSObject"/>
<customObject id="Voe-Tx-rLC" customClass="AppDelegate" customModule="masterkey" customModuleProvider="target"/>
<customObject id="YLy-65-1bz" customClass="NSFontManager"/>
<menu title="Main Menu" systemMenu="main" id="AYu-sK-qS6">
<items>
<menuItem title="Master Key" id="1Xt-HY-uBw">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Master Key" systemMenu="apple" id="uQy-DD-JDr">
<items>
<menuItem title="About masterkey" id="5kV-Vb-QxS">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="orderFrontStandardAboutPanel:" target="-1" id="Exp-CZ-Vem"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="VOq-y0-SEH"/>
<menuItem title="Preferences…" keyEquivalent="," id="BOF-NM-1cW"/>
<menuItem isSeparatorItem="YES" id="wFC-TO-SCJ"/>
<menuItem title="Services" id="NMo-om-nkz">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Services" systemMenu="services" id="hz9-B4-Xy5"/>
</menuItem>
<menuItem isSeparatorItem="YES" id="4je-JR-u6R"/>
<menuItem title="Hide masterkey" keyEquivalent="h" id="Olw-nP-bQN">
<connections>
<action selector="hide:" target="-1" id="PnN-Uc-m68"/>
</connections>
</menuItem>
<menuItem title="Hide Others" keyEquivalent="h" id="Vdr-fp-XzO">
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
<connections>
<action selector="hideOtherApplications:" target="-1" id="VT4-aY-XCT"/>
</connections>
</menuItem>
<menuItem title="Show All" id="Kd2-mp-pUS">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="unhideAllApplications:" target="-1" id="Dhg-Le-xox"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="kCx-OE-vgT"/>
<menuItem title="Quit masterkey" keyEquivalent="q" id="4sb-4s-VLi">
<connections>
<action selector="terminate:" target="-1" id="Te7-pn-YzF"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Help" id="wpr-3q-Mcd">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Help" systemMenu="help" id="F2S-fz-NVQ">
<items>
<menuItem title="Master Key Help" keyEquivalent="?" id="FKE-Sm-Kum">
<connections>
<action selector="showHelp:" target="-1" id="y7X-2Q-9no"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
</items>
</menu>
</objects>
</document>

View File

@@ -0,0 +1,87 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSAppleEventsUsageDescription</key>
<string></string>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDocumentTypes</key>
<array>
<dict>
<key>CFBundleTypeName</key>
<string>Any File</string>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>LSItemContentTypes</key>
<array>
<string>public.content</string>
</array>
</dict>
<dict>
<key>CFBundleTypeExtensions</key>
<array>
<string>pptxmac</string>
</array>
<key>CFBundleTypeName</key>
<string>OSIT PPTX Mac</string>
<key>CFBundleTypeRole</key>
<string>Editor</string>
</dict>
<dict>
<key>CFBundleTypeExtensions</key>
<array>
<string>pptxwin</string>
</array>
<key>CFBundleTypeName</key>
<string>OSIT PPTX Win</string>
<key>CFBundleTypeRole</key>
<string>Editor</string>
</dict>
<dict>
<key>CFBundleTypeExtensions</key>
<array>
<string>pptmac</string>
</array>
<key>CFBundleTypeName</key>
<string>OSIT PPT Mac</string>
<key>CFBundleTypeRole</key>
<string>Editor</string>
</dict>
<dict>
<key>CFBundleTypeExtensions</key>
<array>
<string>pptwin</string>
</array>
<key>CFBundleTypeName</key>
<string>OSIT PPT Win</string>
<key>CFBundleTypeRole</key>
<string>Editor</string>
</dict>
</array>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIconFile</key>
<string></string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.6</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>LSMinimumSystemVersion</key>
<string>$(MACOSX_DEPLOYMENT_TARGET)</string>
<key>NSHumanReadableCopyright</key>
<string>Copyright © 2019 One Sky IT. All rights reserved.</string>
<key>NSMainNibFile</key>
<string>MainMenu</string>
<key>NSPrincipalClass</key>
<string>NSApplication</string>
</dict>
</plist>

View File

@@ -0,0 +1,24 @@
//
// LegacyUtilities.h
// masterkey
//
// Created by Ian Kohl on 4/6/19.
// Copyright © 2019 One Sky IT. All rights reserved.
//
#import <Cocoa/Cocoa.h>
#ifndef LegacyUtilities_h
#define LegacyUtilities_h
@interface LegacyUtilities: NSObject
-(void) setupMirror;
-(void) doMirror;
-(void) undoMirror;
-(void) executeMirrorAction;
@end
#endif /* LegacyUtilities_h */

View File

@@ -0,0 +1,135 @@
//
// LegacyUtilities.m
// masterkey
//
// Created by Ian Kohl on 4/6/19.
// Copyright © 2019 One Sky IT. All rights reserved.
//
#import <Cocoa/Cocoa.h>
#include <Carbon/Carbon.h>
#import "LegacyUtilities.h"
@implementation LegacyUtilities : NSObject
-(void) setupMirror {
CGDisplayCount numberOfActiveDspys;
CGDisplayCount numberOfOnlineDspys;
CGDisplayCount numberOfTotalDspys = 2; // The number of total displays
CGDirectDisplayID activeDspys[] = {0,0};
CGDirectDisplayID onlineDspys[] = {0,0};
CGDirectDisplayID secondaryDspy;
CGDisplayErr activeError = CGGetActiveDisplayList (numberOfTotalDspys,activeDspys,&numberOfActiveDspys);
if (activeError!=0) NSLog(@"Error in obtaining active diplay list: %d\n",activeError);
CGDisplayErr onlineError = CGGetOnlineDisplayList (numberOfTotalDspys,onlineDspys,&numberOfOnlineDspys);
if (onlineError!=0) NSLog(@"Error in obtaining online diplay list: %d\n",onlineError);
CGDisplayConfigRef configRef;
CGError err = CGBeginDisplayConfiguration (&configRef);
if (err != 0) NSLog(@"Error with CGBeginDisplayConfiguration: %d\n",err);
if (numberOfOnlineDspys==2) {
if (onlineDspys[0]==CGMainDisplayID()){
secondaryDspy = onlineDspys[1];
} else {
secondaryDspy = onlineDspys[0];
}
CGDisplayConfigRef configRef;
CGError err = CGBeginDisplayConfiguration (&configRef);
if (err != 0) NSLog(@"Error with CGBeginDisplayConfiguration: %d\n",err);
if (numberOfActiveDspys==2) { // Displays are unmirrored -> mirror them
err = CGConfigureDisplayMirrorOfDisplay (configRef,secondaryDspy,CGMainDisplayID());
NSLog(@"ConfigureMirror Error: %d",err);
}
err = CGCompleteDisplayConfiguration (configRef,kCGConfigurePermanently);
NSLog(@"Mirror CompleteConfig Error: %d",err);
} else {
if (numberOfOnlineDspys>2) {
printf("Cannot handle more than 2 displays at this time. %d displays detected.\n",numberOfOnlineDspys);
} else {
printf("No secondary display detected.\n");
}
}
}
-(void) doMirror {
NSEvent *thisEvent = [NSEvent otherEventWithType:NSEventTypeSystemDefined location:CGPointMake(0,0) modifierFlags:0xa00 timestamp:0 windowNumber:0 context:0 subtype:8 data1:(NX_KEYTYPE_VIDMIRROR<<16|(0xa <<8)) data2:-1];
CGEventRef cThisEvent = [thisEvent CGEvent];
CGEventPost(0, (cThisEvent));
NSEvent *thatEvent = [NSEvent otherEventWithType:NSEventTypeSystemDefined location:NSMakePoint(0,0) modifierFlags:0xb00 timestamp:0 windowNumber:0 context:[NSGraphicsContext currentContext] subtype:8 data1:(NX_KEYTYPE_VIDMIRROR<<16|(0xb <<8)) data2:-1];
CGEventRef cThatEvent = [thatEvent CGEvent];
CGEventPost(0, (cThatEvent));
}
-(void) undoMirror {
enum MirrorMode {
off,
} mode;
mode = off;
CGDisplayCount numberOfActiveDspys;
CGDisplayCount numberOfOnlineDspys;
CGDisplayCount numberOfTotalDspys = 2;
CGDirectDisplayID activeDspys[] = {0,0};
CGDirectDisplayID onlineDspys[] = {0,0};
CGDirectDisplayID secondaryDspy;
CGDisplayErr activeError = CGGetActiveDisplayList (numberOfTotalDspys,activeDspys,&numberOfActiveDspys);
if (activeError!=0) NSLog(@"Error in obtaining active diplay list: %d\n",activeError);
CGDisplayErr onlineError = CGGetOnlineDisplayList (numberOfTotalDspys,onlineDspys,&numberOfOnlineDspys);
if (onlineError!=0) NSLog(@"Error in obtaining online diplay list: %d\n",onlineError);
if (numberOfOnlineDspys==2) { // Online displays = physical displays regardless of mirror status
if (onlineDspys[0]==CGMainDisplayID()){
secondaryDspy = onlineDspys[1];
} else {
secondaryDspy = onlineDspys[0];
}
switch (mode) {
case off:
if (numberOfActiveDspys!=2) // Active displays = software displays (mirror = 1)
[self executeMirrorAction];
break;
default:
break;
}
} else {
if (numberOfOnlineDspys>2) {
printf("Cannot handle more than 2 displays at this time. %d displays detected.\n",numberOfOnlineDspys);
} else {
printf("No secondary display detected.\n");
}
}
}
-(void) executeMirrorAction {
NSEvent *thisEvent = [NSEvent otherEventWithType:NSEventTypeSystemDefined location:CGPointMake(0,0) modifierFlags:0xa00 timestamp:0 windowNumber:0 context:0 subtype:8 data1:(NX_KEYTYPE_VIDMIRROR<<16|(0xa <<8)) data2:-1];
CGEventRef cThisEvent = [thisEvent CGEvent];
CGEventPost(0, (cThisEvent));
NSEvent *thatEvent = [NSEvent otherEventWithType:NSEventTypeSystemDefined location:NSMakePoint(0,0) modifierFlags:0xb00 timestamp:0 windowNumber:0 context:[NSGraphicsContext currentContext] subtype:8 data1:(NX_KEYTYPE_VIDMIRROR<<16|(0xb <<8)) data2:-1];
CGEventRef cThatEvent = [thatEvent CGEvent];
CGEventPost(0, (cThatEvent));
}
@end

View File

@@ -0,0 +1,41 @@
//
// Utilities.swift
// masterkey
//
// Created by Ian Kohl on 4/6/19.
// Copyright © 2019 One Sky IT. All rights reserved.
//
import Foundation
class Utilities {
func startRecording(filename: String, num_screens: Int) -> String
{
//Create output filename
let timestamp = String(NSDate().timeIntervalSince1970)
let home = NSHomeDirectory()
let recording_filename = timestamp + filename + ".mkv"
let recording_path = "\(home)/recordings/\(recording_filename)"
let device_string = "\"\(num_screens):0\""
//Get Devices
//ffmpeg -f avfoundation -list_devices true -i ""
//Start recording
//ffmpeg -f avfoundation -i "2:<audio device index>" ~/recordings/filename.mkv
let task:Process = Process()
let pipe:Pipe = Pipe()
task.launchPath = "/usr/local/bin/ffmpeg"
task.arguments = ["-f","avfoundation","-i",device_string,recording_path]
task.standardOutput = pipe
task.launch()
let handle = pipe.fileHandleForReading
let data = handle.readDataToEndOfFile()
let result_s = String(data: data, encoding: String.Encoding.utf8)!
return result_s
}
}

View File

@@ -0,0 +1,3 @@
Microsoft Office PowerPoint
VLC Windows
Acrobat Reader Windows

View File

@@ -0,0 +1,5 @@
//
// Use this file to import your target's public headers that you would like to expose to Swift.
//
#import "LegacyUtilities.h"

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict/>
</plist>

View File

@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSAppleEventsUsageDescription</key>
<string></string>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>BNDL</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>1</string>
</dict>
</plist>

View File

@@ -0,0 +1,34 @@
//
// masterkeyTests.swift
// masterkeyTests
//
// Created by Ian Kohl on 2/28/19.
// Copyright © 2019 One Sky IT. All rights reserved.
//
import XCTest
@testable import masterkey
class masterkeyTests: XCTestCase {
override func setUp() {
// Put setup code here. This method is called before the invocation of each test method in the class.
}
override func tearDown() {
// Put teardown code here. This method is called after the invocation of each test method in the class.
}
func testExample() {
// This is an example of a functional test case.
// Use XCTAssert and related functions to verify your tests produce the correct results.
}
func testPerformanceExample() {
// This is an example of a performance test case.
self.measure {
// Put the code you want to measure the time of here.
}
}
}

582
index.js
View File

@@ -1,582 +0,0 @@
const { app, BrowserWindow, ipcMain, shell, systemPreferences } = require('electron');
const axios = require('axios');
const crypto = require('crypto');
const fs = require('fs');
const os = require('os');
const path = require('path');
const process = require('process');
//const http = require('http');
//const request = require('request');
//const url = require('url');
// const usb = require('usb') // Compiled with an old version of Node.js
console.log(`OS: ${os.type()} ${process.getSystemVersion()}`);
// console.log(process.getSystemVersion());
let home_directory = require('os').homedir();
console.log('Home: '+home_directory);
let tmp_directory = require('os').tmpdir();
console.log('Temporary: '+tmp_directory);
// Set the config path for macOS or Linux
let config_directory = 'OSIT/native_app';
let config_filename = 'ae_native_app_config.json';
let config_path = '';
let local_file_cache_path = null;
let host_file_temp_path = null;
let endpoints_in_progress = [];
/* Look for and load a JSON formatted config file. */
if (os.platform == 'darwin') {
let config_path_default = path.join(home_directory, config_directory, config_filename);
let config_path_macos = path.join(home_directory, 'Library/Application Support/OSIT', config_filename);
// let config_path_opt2 = path.join(home_directory, 'OSIT', config_filename);
if (fs.existsSync(config_path_default)) {
console.log('Default config file path exists: '+config_path_default);
config_path = config_path_default;
} else if (fs.existsSync(config_path_macos)) {
console.log('macOS config file path exists: '+config_path_macos);
config_path = config_path_macos;
} else {
console.log(`No config file found: ${config_path_default} or ${config_path_macos}`);
config_path = '';
// fs.mkdirSync(config_file_directory_path, true);
// console.log('Config directory path created: '+config_file_directory_path);
}
console.log(`Using config found on macOS: ${config_path}`);
} else if (os.platform == 'linux') {
let config_path_default = path.join(home_directory, config_directory, config_filename);
let config_path_linux_os = path.join(home_directory, '.config/OSIT', config_filename);
let config_path_temp = path.join(home_directory, 'tmp/OSIT', config_filename);
if (fs.existsSync(config_path_default)) {
console.log('Default config file path exists: '+config_path_default);
config_path = config_path_default;
} else if (fs.existsSync(config_path_linux_os)) {
console.log('Linux config file path exists: '+config_path_linux_os);
config_path = config_path_linux_os;
} else if (fs.existsSync(config_path_temp)) {
console.log('Temp config file path exists: '+config_path_temp);
config_path = config_path_temp;
} else {
console.log(`No config file found: ${config_path_default} or ${config_path_linux_os} or ${config_path_temp}`);
config_path = '';
}
console.log(`Using config found on Linux: ${config_path}`);
}
let config = JSON.parse(fs.readFileSync(config_path));
console.log('Config file read.', config);
/*
Minimal configuration contains:
* conf_file_check_path = '~/OSIT/sync/admin_share/internal/ae_osit_app.default.conf'
* conf_file_check_path_backup = 'ae_osit_app.conf'
* api_pref_use = 'local' or 'remote' or 'backup'
* api_base_url_local = https://local-api.oneskyit.com
* api_base_url_remote = https://api.oneskyit.com
* api_base_url_backup = https://bak-api.oneskyit.com
* app_pref_use = 'local' or 'remote' or 'backup'
* app_base_url_local = https://local-demo.oneskyit.com
* app_base_url_remote = https://demo.oneskyit.com
* app_base_url_backup = https://bak-demo.oneskyit.com
* device_id = 'abcd1234'
*/
/*
Ask for permissions from macOS to use the microphone, screen, camera. The OS may delay actually asking for permission until the permission is actually attempted to be used. It may be worth doing a test attempt early on if access has not already been granted. -STI 2023-06-03
*/
if (os.type == 'Darwin') {
if (systemPreferences.getMediaAccessStatus('microphone') != 'granted') {
systemPreferences.askForMediaAccess('microphone');
} else {
console.log('Microphone access:', systemPreferences.getMediaAccessStatus('microphone'));
}
if (systemPreferences.getMediaAccessStatus('screen') != 'granted') {
systemPreferences.askForMediaAccess('screen');
} else {
console.log('Screen access:', systemPreferences.getMediaAccessStatus('screen'));
}
if (systemPreferences.getMediaAccessStatus('camera') != 'granted') {
systemPreferences.askForMediaAccess('camera');
} else {
console.log('Camera access:', systemPreferences.getMediaAccessStatus('camera'));
}
}
async function get_url_cfg() {
let base_url = `${config.api_protocol}://${config.api_server}:${config.api_port}/${config.api_path}`;
let axios_api = axios.create({
baseURL: base_url,
timeout: 60000, // in milliseconds; 60000 = 60 seconds
/* other custom settings */
});
// axios_api.defaults.headers = config['headers'];
// axios.defaults.baseURL = `${config.api_protocol}://${config.api_server}:${config.api_port}/${config.api_path}`;
axios_api.defaults.headers.common['Access-Control-Allow-Origin'] = config.access_control_allow_origin; // '*'; // app_config.access_control_allow_origin;
axios_api.defaults.headers.common['content-type'] = 'application/json';
axios_api.defaults.headers.common['x-aether-api-key'] = config.api_secret_key;
axios_api.defaults.headers.common['x-account-id'] = config.account_id;
let event_device_id = 'dbgMWS3KEHE';
let endpoint = `/event/device/${event_device_id}`;
let params = {'event_device_code': 'asdf'};
let response_data_promise = await axios_api.get(
endpoint,
{
params: params,
onDownloadProgress: (progressEvent) => {
let percent_completed = Math.round(
(progressEvent.loaded * 100) / progressEvent.total
);
console.log('GET Data Timestamp:', progressEvent.timeStamp, 'Total:', progressEvent.total, 'Loaded:', progressEvent.loaded, 'Percent Completed', percent_completed);
// temp_get_object_percent_completed = percent_completed;
}
}
)
.then(function (response) {
console.log(`Response: ${response}`);
let return_data = response.data['data'];
if (Array.isArray(return_data)) {
console.log(`Data result is an array/list. Array length: ${return_data.length}`);
} else {
console.log(`Data result is a dictionary/object, not an array/list.`);
}
return return_data;
})
.catch(function (error) {
console.log(`Base URL: ${base_url} | Endpoint: ${endpoint}`);
console.log('Error Message:', error.message); // Is this needed here or below in the in the else portion???
if (error.response) {
console.log(`Response Status: ${error.response.status}; Status Text: ${error.response.statusText}`);
} else {
console.log('Error:', error);
}
if (error.response && error.response.status === 404) {
return null; // Returning null since there were no results
}
return false; // Returning false since something may have gone wrong. Also more in line with what the API returns.
});
return response_data_promise;
}
let new_config = get_url_cfg();
console.log(new_config);
function createWindow () {
// Create the browser window.
win = new BrowserWindow({
width: 1500, // 1500 1280
height: 1024, // 1024
backgroundColor: '#aaa',
icon: './app/img/favicon.ico',
webPreferences: {
contextIsolation: false,
nodeIntegration: true,
nodeIntegrationInWorker: true,
enableRemoteModule: true
}
})
// win.setMinimumSize(1024, 768);
// win.setMinimumSize(1280, 768);
win.setMinimumSize(1400, 768);
//win.setFullScreenable(false)
win.FullScreenable = false;
// win.webContents.session.clearStorageData(['filesystem']); // Does this do anything???
// native_app_which_html = 'default', 'path', 'url'
// 'default' (internal) is within the bundled native app
// 'path' (external to app) is a file path on the host, probably under the home directory
// 'url' is over HTTPS, maybe onsite or offsite
// Load the index.html of the app
if (config.native_app_which_html == '' || config.native_app_which_html == 'default') {
win.loadFile('app/index.html');
} else if (config.native_app_which_html == 'path') {
let index_path = 'index.html';
if (config.native_app_index_path) {
index_path = config.native_app_index_path.replace('[home]', home_directory);
} else {
index_path = path.join(home_directory, 'OSIT/native_app/app/index.html');
}
win.loadFile(index_path);
} else if (config.native_app_which_html == 'url') {
let index_url = 'http://localhost/index.html';
if (config.native_app_index_url) {
index_url = config.native_app_index_url;
} else {
index_url = 'https://app.oneskyit.com/native/index.html';
}
win.loadURL(index_url);
} else {
win.loadFile('app/index.html');
}
// Open the DevTools.
if (config.developer_tools) {
win.webContents.openDevTools(); // Comment out for production
}
// Emitted when the window is closed.
win.on('closed', () => {
// Dereference the window object, usually you would store windows
// in an array if your app supports multi windows, this is the time
// when you should delete the corresponding element.
win = null;
})
win.on('minimize', () => {
//win.restore();
})
}
// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.on('ready', createWindow);
// Quit when all windows are closed.
app.on('window-all-closed', () => {
// On macOS it is common for applications and their menu bar
// to stay active until the user quits explicitly with Cmd + Q
if (process.platform !== 'darwin') {
app.quit();
}
})
app.on('activate', () => {
// On macOS it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open.location_files
if (win === null) {
createWindow();
}
})
// Import config data
// Updated 2022-04-16
ipcMain.handle('import_config', async (event, config_data) => {
console.log('*** Electron IPC Main: import_config() ***');
// console.log('ipcMain on download_file: api_base_url='+api_base_url+' | api_temporary_token='+api_temporary_token);
console.log('ipcMain on import_config:');
console.log(config_data);
config = config_data;
local_file_cache_path = config.local_file_cache_path;
host_file_temp_path = config.host_file_temp_path;
if (fs.existsSync(local_file_cache_path)) {
console.log('Host file cache path exists: '+local_file_cache_path);
} else {
fs.mkdirSync(local_file_cache_path, true);
console.log('Host file cache path created: '+local_file_cache_path);
}
if (fs.existsSync(host_file_temp_path)) {
console.log('Host file temp path exists: '+host_file_temp_path);
} else {
fs.mkdirSync(host_file_temp_path, true);
console.log('Host file temp path created: '+host_file_temp_path);
}
return true;
});
// Download file to path
// full_save_path should be the full path that includes the filename
// Updated 2023-05-14
ipcMain.handle('download_file', async (event, api_base_url, api_endpoint, full_save_path, hash=null, verify_hash=false, overwrite_existing=false, offset_minutes=3) => {
console.log('*** Electron IPC Main: download_file() ***');
// console.log('ipcMain on download_file: api_base_url='+api_base_url+' | api_temporary_token='+api_temporary_token);
// console.log('ipcMain on download_file: api_base_url='+api_base_url);
console.log(`ipcMain download and save file: HTTP ${api_endpoint} -> FILE ${full_save_path}`);
if (!api_base_url) {
console.log('API Base URL is required. Returning false');
return false;
}
axios.defaults.baseURL = api_base_url;
axios.defaults.headers.common['Access-Control-Allow-Origin'] = config.access_control_allow_origin; // '*'; // app_config.access_control_allow_origin;
axios.defaults.headers.common['content-type'] = 'application/json';
axios.defaults.headers.common['x-aether-api-key'] = config.api_secret_key;
axios.defaults.headers.common['x-account-id'] = config.account_id;
const url = api_endpoint;
const tmp_full_save_path = full_save_path+'.tmp';
if (fs.existsSync(tmp_full_save_path)) {
console.log(`A temp download file was found! ${tmp_full_save_path}`);
let stats = null;
try {
stats = fs.statSync(tmp_full_save_path);
// console.log(`File Accessed Last: ${stats.atime}`); // File data last changed (actual contents)
console.log(`File Data Last Modified: ${stats.mtime}`); // File data last changed (actual contents)
console.log(`File Metadata Last Modified: ${stats.ctime}`); // File metadata last changed (filename, permissions, etc)
} catch (error) {
console.log(error);
}
let current_datetime = new Date();
// In minutes. After 5ish minutes of no changes to the file content seems reasonable? Tested with 3 minutes for multiple meetings and no noticable problem.
let offset_datetime = new Date(current_datetime.getTime() - offset_minutes*60000);
// console.log(`Times: ${current_datetime} ${offset_datetime} | File ${stats.mtime}`);
if (stats.mtime < offset_datetime) {
console.log(`Marking as expired temp file based on modified datetime. Expire after: ${offset_minutes} minutes`);
overwrite_existing = true;
} else {
console.log(`Temp download file has not expired yet. Expire after: ${offset_minutes} minutes`);
// return false;
return 'tmp';
}
}
if (fs.existsSync(full_save_path)) {
console.log(`A cached file was found! ${full_save_path}`);
if (verify_hash) {
const file_buffer = fs.readFileSync(full_save_path);
const file_hash_sha256 = crypto.createHash('sha256');
file_hash_sha256.update(file_buffer);
const file_hash_sha256_check = file_hash_sha256.digest('hex');
if (file_hash_sha256_check == hash) {
// console.log('File hash match', file_hash_sha256_check);
} else {
console.log('File hash does not match', file_hash_sha256_check);
if (overwrite_existing) {
console.log('Going to overwrite the existing file because the hash does not match.');
} else {
return false;
}
}
}
// return false;
}
console.log('Endpoints in Progress:', endpoints_in_progress);
if (endpoints_in_progress.includes(api_endpoint)) {
console.log(`Endpoint already being downloaded: ${api_endpoint}`);
// return false;
return 'in_progress';
}
// console.log(`Done with checks. Time to download! Endpoint: ${api_endpoint}`);
endpoints_in_progress.push(api_endpoint);
let download_result = await axios({
method: 'get',
url: url,
responseType: 'stream' /* responseType must be stream */
}).then(function (response) {
console.log(`Creating write stream for downloading endpoint: ${api_endpoint}`);
const writer = fs.createWriteStream(tmp_full_save_path);
return new Promise((resolve, reject) => {
response.data.pipe(writer);
let error = null;
writer.on('error', err => {
console.log('Writer error!');
error = err;
console.log(error);
writer.close();
reject(err);
});
writer.on('close', () => {
if (!error) {
// console.log(`Download complete! Writer closed.`);
resolve(true);
} else {
console.log('Writer closed unexpectedly!', error);
}
//no need to call the reject here, as it will have been called in the
//'error' stream;
});
});
})
.then(function (response) {
console.log(`Download complete. Temporary file moved/renamed: ${full_save_path}`);
fs.renameSync(tmp_full_save_path, full_save_path);
for( let i = 0; i < endpoints_in_progress.length; i++){
if ( endpoints_in_progress[i] === api_endpoint) {
endpoints_in_progress.splice(i, 1);
// NOTE: Decrement the index variable so it does not skip the next item in the array.
i--;
}
}
return true;
})
.catch(function (error) {
console.log(`Error downloading! Endpoint: ${api_endpoint}`);
for( let i = 0; i < endpoints_in_progress.length; i++){
if ( endpoints_in_progress[i] === api_endpoint) {
endpoints_in_progress.splice(i, 1);
// NOTE: Decrement the index variable so it does not skip the next item in the array.
i--;
}
}
if (error.response) {
console.log(`Response Status: ${error.response.status}; Status Text: ${error.response.statusText}`);
} else {
console.log('Error:', error);
}
if (error.response && error.response.status === 404) {
return null; // Returning null since there were no results
}
return false; // Returning false since something may have gone wrong. Also more in line with what the API returns.
});
// console.log(`Done with download function! Endpoint: ${api_endpoint}`);
return download_result;
});
ipcMain.handle('open_hash_file_to_temp', async (event, local_file_cache_path, hash, host_file_temp_path, filename, verify_hash=true) => {
console.log('*** Electron IPC Main: open_hash_file_to_temp() ***');
console.log('ipcMain on open_hash_file_to_temp');
console.log(`ipcMain open hash file from temp directory: ${local_file_cache_path} -> ${host_file_temp_path}/${filename}`);
// NOTE: This may be needed later? Uncomment if paths are relative to working directory.
// let cache_file_path = path.join(process.cwd(), local_file_cache_path);
let cache_file_path = local_file_cache_path;
console.log(cache_file_path);
let hash_filename = hash+'.file';
let full_cache_file_path = path.join(cache_file_path, hash_filename);
console.log(full_cache_file_path);
// NOTE: This may be needed later? Uncomment if paths are relative to working directory.
// open_temp_file_path = path.join(process.cwd(), host_file_temp_path, filename); // 'temp/'
open_temp_file_path = path.join(host_file_temp_path, filename); // 'temp/'
console.log(open_temp_file_path);
if (fs.existsSync(open_temp_file_path)) {
console.log('A file with the same name already exists in the local temp directory: '+open_temp_file_path);
// NOTE: Should the file be checked to see if it has changed from the hashed cache version???
// NOTE: What if they made changes to the file locally in temp? The changed file would be used since a new copy is not being made.
// NOTE: It might make sense for this to be a configurable option depending on the group. Some do not allow changes. This helps enforce that.
}
if (fs.existsSync(full_cache_file_path)) {
// console.log(`Hashed file exists in cache: ${full_cache_file_path}`);
console.log(`Copying file to temp: ${open_temp_file_path}`);
try {
fs.copyFileSync(full_cache_file_path, open_temp_file_path);
} catch (error) {
console.error(error);
return false;
}
if (verify_hash) {
const file_buffer = fs.readFileSync(full_cache_file_path);
const file_hash_sha256 = crypto.createHash('sha256');
file_hash_sha256.update(file_buffer);
const file_hash_sha256_check = file_hash_sha256.digest('hex');
if (file_hash_sha256_check == hash) {
// console.log('File hash match', file_hash_sha256_check);
} else {
console.log('File hash does not match', file_hash_sha256_check);
return false;
}
}
// console.log('Creating file link: '+open_temp_file_path);
// fs.linkSync(full_cache_file_path, open_temp_file_path);
} else {
console.log(`Hashed file not found in cache: ${full_cache_file_path}`);
return false;
}
try {
await shell.openPath(open_temp_file_path);
} catch (error) {
console.error(error);
return false;
}
console.log('End: Electron IPC Main: open_hash_file_to_temp()');
return true;
});
ipcMain.handle('open_local_file', async (event, local_file_path, filename, use_cwd=true) => {
console.log('*** Electron IPC Main: open_local_file() ***');
console.log('ipcMain on open_local_file');
console.log(`ipcMain open local file from directory: ${local_file_path}/${filename}`);
let full_local_file_path = null;
if (use_cwd) {
full_local_file_path = path.join(process.cwd(), local_file_path, filename);
console.log(full_local_file_path);
} else {
full_local_file_path = path.join(local_file_path, filename);
console.log(full_local_file_path);
}
if (fs.existsSync(full_local_file_path)) {
console.log(`Local file exists: ${full_local_file_path}`);
} else {
console.log(`Local file not found: ${full_local_file_path}`);
return false;
}
try {
await shell.openPath(full_local_file_path);
} catch (error) {
console.error(error);
return false;
}
console.log('End: Electron IPC Main: open_local_file()');
return true;
});

3965
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,25 +1,25 @@
{
"name": "aether_app_native",
"productName": "Aether App: Native",
"version": "2023.5.14.2",
"description": "One Sky IT's Native Aether App",
"main": "index.js",
"name": "aether_app_native_electron",
"version": "1.0.0",
"description": "AE Native Launcher V3",
"main": "dist/main/index.js",
"scripts": {
"start": "electron .",
"start_nogpu": "electron . --disable-gpu",
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "Scott Idem",
"license": "ISC",
"dependencies": {
"axios": "^1.1.2",
"child_process": "^1.0.2",
"fs": "^0.0.1-security",
"os": "^0.1.2",
"path": "^0.12.7"
"postinstall": "node scripts/patch-packager-unzip.js",
"start": "tsc && electron .",
"dev": "tsc && electron .",
"build": "tsc",
"watch": "tsc -w",
"package:linux": "tsc && electron-packager . aether_launcher --platform=linux --arch=x64 --out=builds --overwrite --prune=true",
"package:mac": "tsc && electron-packager . aether_launcher --platform=darwin --arch=x64,arm64 --out=builds --overwrite --prune=true --icon=resources/img/osit_logo.icns"
},
"devDependencies": {
"electron": "^22.3.6",
"electron-packager": "^16.0.0"
"@types/node": "^22.19.0",
"electron": "^42.0.1",
"@electron/packager": "^20.0.0",
"ts-node": "^10.9.2",
"typescript": "^5.9.3"
},
"dependencies": {
"axios": "^1.13.2"
}
}

View File

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

Before

Width:  |  Height:  |  Size: 9.5 KiB

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

View File

Before

Width:  |  Height:  |  Size: 9.5 KiB

After

Width:  |  Height:  |  Size: 9.5 KiB

View File

@@ -0,0 +1,23 @@
#!/usr/bin/env node
// extract-zip/yauzl streams hang on Node 26 (no data events emitted).
// bsdtar (libarchive) handles both Linux and macOS zips including chained symlinks in .app bundles.
const { writeFileSync, existsSync } = require('fs');
const { resolve } = require('path');
const target = resolve('node_modules/@electron/packager/dist/unzip.js');
if (!existsSync(target)) {
console.log('patch-packager-unzip: target not found, skipping');
process.exit(0);
}
const patched = `import { execSync } from 'node:child_process';
// extract-zip/yauzl streams are broken on Node 26; use bsdtar (libarchive) instead.
// bsdtar correctly handles chained symlinks in macOS .app bundles that 7z refuses.
export async function extractElectronZip(zipPath, targetDir) {
execSync(\`bsdtar -xf "\${zipPath}" -C "\${targetDir}"\`, { stdio: 'pipe' });
}
//# sourceMappingURL=unzip.js.map
`;
writeFileSync(target, patched);
console.log('patch-packager-unzip: patched @electron/packager/dist/unzip.js to use bsdtar');

75
src/main/api_client.ts Normal file
View File

@@ -0,0 +1,75 @@
import { SeedConfig } from '../shared/types';
export async function fetchFullConfig(seed: SeedConfig): Promise<any> {
const apiUrls = [
seed.onsite_api_base_url,
seed.primary_api_base_url,
seed.backup_api_base_url
].filter(url => url !== null && url !== undefined) as string[];
let lastError: any = null;
for (const baseUrl of apiUrls) {
try {
console.log(`Bootstrap: Attempting connection to ${baseUrl}...`);
// --- STEP 1: Get Device Config ---
const deviceUrl = `${baseUrl}/v3/crud/event_device/${seed.event_device_id}`;
const deviceResponse = await fetch(deviceUrl, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'x-aether-api-key': seed.aether_api_key,
'x-no-account-id': 'bypass'
},
});
if (!deviceResponse.ok) {
throw new Error(`Device lookup failed (${deviceResponse.status})`);
}
const deviceResult = await deviceResponse.json();
const deviceData = deviceResult.data || deviceResult;
// Use 'app_base_url' as the FQDN for the site lookup
const fqdn = deviceData.app_base_url || 'native-demo.oneskyit.com';
// --- STEP 2: Get Site Context ---
const searchUrl = `${baseUrl}/v3/crud/site_domain/search?limit=1`;
const siteResponse = await fetch(searchUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-aether-api-key': seed.aether_api_key,
'x-account-id': deviceData.account_id_random || deviceData.account_id || ''
},
body: JSON.stringify({
and: [{ field: 'fqdn', op: 'eq', value: fqdn }]
})
});
if (!siteResponse.ok) {
throw new Error(`Site context lookup failed (${siteResponse.status})`);
}
const siteResult = await siteResponse.json();
const siteDomain = (siteResult.data && siteResult.data.length > 0) ? siteResult.data[0] : null;
console.log(`Bootstrap Success using ${baseUrl}`);
return {
...siteDomain,
native_device: deviceData,
aether_api_key: seed.aether_api_key // Include the key for frontend use
};
} catch (error) {
console.warn(`Bootstrap failed for ${baseUrl}: `, error);
lastError = error;
continue; // Try next URL
}
}
console.error('Bootstrap Critical Failure: All API endpoints exhausted.', lastError);
return null;
}

30
src/main/config_loader.ts Normal file
View File

@@ -0,0 +1,30 @@
import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
import { SeedConfig } from '../shared/types';
export async function loadSeedConfig(): Promise<SeedConfig | null> {
// For development, we look in the home directory
const configPath = path.join(os.homedir(), 'seed.json');
try {
if (!fs.existsSync(configPath)) {
console.log(`Seed config not found at: ${configPath}`);
return null;
}
const data = fs.readFileSync(configPath, 'utf-8');
const config = JSON.parse(data) as SeedConfig;
// Basic validation
if (!config.event_device_id) {
console.error('Invalid seed config: missing event_device_id');
return null;
}
return config;
} catch (error) {
console.error('Error loading seed config:', error);
return null;
}
}

188
src/main/file_handlers.ts Normal file
View File

@@ -0,0 +1,188 @@
import { ipcMain } from 'electron';
import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
import * as crypto from 'crypto';
import { exec } from 'child_process';
import axios from 'axios';
import { expandPath } from './file_utils';
let endpoints_in_progress: string[] = [];
export function registerFileHandlers() {
// Flexible organization: [root]/[prefix_len-char-prefix]/[hash].file
function get_organized_hashed_path(root: string, hash: string, prefix_len: number = 2) {
const expanded_root = expandPath(root);
const prefix = hash.substring(0, Math.max(1, Math.min(prefix_len, 8)));
const dir = path.join(expanded_root, prefix);
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
return path.join(dir, `${hash}.file`);
}
ipcMain.handle('native:check-cache', async (event, { cache_root, hash, hash_prefix_length = 2, verify_hash = false }) => {
const full_path = get_organized_hashed_path(cache_root, hash, hash_prefix_length);
if (!fs.existsSync(full_path)) return false;
if (verify_hash) {
try {
const file_buffer = fs.readFileSync(full_path);
const actual_hash = crypto.createHash('sha256').update(file_buffer).digest('hex');
return actual_hash === hash;
} catch (e) {
return false;
}
}
return true;
});
ipcMain.handle('native:download-to-cache', async (event, { url, cache_root, hash, api_key, account_id, hash_prefix_length = 2 }) => {
const full_path = get_organized_hashed_path(cache_root, hash, hash_prefix_length);
const tmp_path = `${full_path}.tmp`;
if (endpoints_in_progress.includes(url)) return { success: true, status: 'in_progress' };
// 1. If final file exists, skip
if (fs.existsSync(full_path)) return { success: true, path: full_path, status: 'exists' };
// 2. Handle stale .tmp files (Legacy "Trust No One" pattern)
if (fs.existsSync(tmp_path)) {
const stats = fs.statSync(tmp_path);
const age_ms = Date.now() - stats.mtimeMs;
// If the tmp file is older than 5 minutes, assume previous download crashed and delete it
if (age_ms > 5 * 60 * 1000) {
console.log(`Native: Deleting stale temp file (${Math.round(age_ms/1000)}s old)`);
fs.unlinkSync(tmp_path);
} else {
return { success: true, status: 'in_progress', detail: 'fresh_tmp_exists' };
}
}
console.log(`Native: Hardened Download -> ${full_path}`);
try {
endpoints_in_progress.push(url);
const response = await axios({
method: 'get', url, responseType: 'stream',
headers: {
'x-aether-api-key': api_key,
'x-account-id': account_id || ''
}
});
const writer = fs.createWriteStream(tmp_path);
response.data.pipe(writer);
await new Promise<void>((resolve, reject) => {
writer.on('finish', () => resolve());
writer.on('error', reject);
});
// 3. Verify Integrity before renaming (The "Trust No One" Check)
const file_buffer = fs.readFileSync(tmp_path);
const actual_hash = crypto.createHash('sha256').update(file_buffer).digest('hex');
if (actual_hash !== hash) {
console.error(`Native: Hash Mismatch! Expected ${hash}, got ${actual_hash}`);
fs.unlinkSync(tmp_path);
return { success: false, error: 'Integrity check failed: Hash mismatch' };
}
fs.renameSync(tmp_path, full_path);
console.log(`Native: Cache Integrity Verified. File moved to final destination.`);
return { success: true, path: full_path };
} catch (error: any) {
if (fs.existsSync(tmp_path)) fs.unlinkSync(tmp_path);
return { success: false, error: error.message };
} finally {
endpoints_in_progress = endpoints_in_progress.filter(e => e !== url);
}
});
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);
const target = path.join(expanded_temp, filename);
console.log(`Native: Launching from Cache -> ${filename}`);
if (!fs.existsSync(source)) {
return { success: false, error: `File not in cache: ${hash}` };
}
if (!fs.existsSync(expanded_temp)) fs.mkdirSync(expanded_temp, { recursive: true });
// 1. Copy the file to temp folder with original name
fs.copyFileSync(source, target);
// 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 (!native_template) {
return { success: false, error: 'No native template configured for this file' };
}
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() });
});
});
}
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 };
}
});
// Thin primitive: copy a cached file to the temp directory with its original filename,
// then return the resolved path. The caller (Svelte side) decides what to do next —
// run_osascript, run_cmd, open_local_file, etc.
//
// This is the preferred building block for custom launch flows. Use launch_from_cache
// 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 {
const source = get_organized_hashed_path(cache_root, hash, hash_prefix_length);
if (!fs.existsSync(source)) {
return { success: false, error: `File not in cache: ${hash}` };
}
const expanded_temp = expandPath(temp_root);
const target = path.join(expanded_temp, filename);
if (!fs.existsSync(expanded_temp)) fs.mkdirSync(expanded_temp, { recursive: true });
fs.copyFileSync(source, target);
console.log(`Native: Copied from cache to temp -> ${target}`);
return { success: true, path: target };
} catch (error: any) {
return { success: false, error: error.message };
}
});
}

16
src/main/file_utils.ts Normal file
View File

@@ -0,0 +1,16 @@
import * as os from 'os';
import * as path from 'path';
export function expandPath(filePath: string): string {
if (!filePath) return filePath;
// Resolve all instances of [home] and [tmp] using global regex
return filePath
.replace(/\[home\]/g, os.homedir())
.replace(/\[tmp\]/g, os.tmpdir());
}
export function getHashedPath(cacheRoot: string, hash: string): string {
const expandedRoot = expandPath(cacheRoot);
const subdirectory = hash.substring(0, 2);
return path.join(expandedRoot, subdirectory, `${hash}.file`);
}

94
src/main/index.ts Normal file
View File

@@ -0,0 +1,94 @@
import { app, BrowserWindow, ipcMain } from 'electron';
import * as path from 'path';
import * as os from 'os';
import { loadSeedConfig } from './config_loader';
import { fetchFullConfig } from './api_client';
import { registerShellHandlers } from './shell_handlers';
import { registerFileHandlers } from './file_handlers';
import { registerSystemHandlers } from './system_handlers';
import { SeedConfig } from '../shared/types';
let mainWindow: BrowserWindow | null = null;
let cachedSeed: SeedConfig | null = null;
let cachedFullConfig: any = null;
async function createWindow() {
cachedSeed = await loadSeedConfig();
if (cachedSeed) {
cachedFullConfig = await fetchFullConfig(cachedSeed);
}
mainWindow = new BrowserWindow({
width: 1600,
height: 900,
title: 'OSIT Aether Launcher (Native)',
webPreferences: {
preload: path.join(__dirname, '../preload/index.js'),
contextIsolation: true,
nodeIntegration: false,
},
});
let targetUrl = 'http://demo.localhost:5173';
if (cachedFullConfig && cachedFullConfig.native_device) {
const device = cachedFullConfig.native_device;
const eventId = device.event_id_random || device.event_id;
const locationId = device.event_location_id_random || device.event_location_id || '';
// Use app_base_url from the device record (e.g. bgh.oneskyit.com).
// Fall back to localhost only if nothing is configured — never override a real domain.
const host = device.app_base_url || 'demo.localhost:5173';
// Use https for real domains; localhost dev URLs stay on http
const protocol = host.includes('localhost') ? 'http' : 'https';
targetUrl = `${protocol}://${host}/events/${eventId}/launcher/${locationId}`;
}
// Only open DevTools in development (not in a packaged .app build)
if (!app.isPackaged) mainWindow.webContents.openDevTools();
mainWindow.loadURL(targetUrl).catch(() => {
mainWindow?.loadURL('https://dev-demo.oneskyit.com/');
});
mainWindow.on('closed', () => { mainWindow = null; });
}
registerShellHandlers();
registerFileHandlers();
registerSystemHandlers();
app.on('ready', createWindow);
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') app.quit();
});
app.on('activate', () => {
if (mainWindow === null) createWindow();
});
ipcMain.handle('get-seed-config', async () => cachedSeed || await loadSeedConfig());
ipcMain.handle('get-device-config', async () => cachedFullConfig);
ipcMain.handle('get-jwt', async () => null);
ipcMain.handle('get-device-info', async () => {
const interfaces = os.networkInterfaces();
const addresses: string[] = [];
for (const name of Object.keys(interfaces)) {
for (const net of interfaces[name]!) {
if (net.family === 'IPv4' && !net.internal) {
addresses.push(net.address);
}
}
}
return {
platform: os.platform(),
release: os.release(),
arch: os.arch(),
hostname: os.hostname(),
cpus: os.cpus().length,
total_mem: os.totalmem(),
free_mem: os.freemem(),
ip_addresses: addresses,
home_directory: os.homedir(),
tmp_directory: os.tmpdir()
};
});

236
src/main/shell_handlers.ts Normal file
View File

@@ -0,0 +1,236 @@
import { ipcMain, shell } from 'electron';
import { exec, execSync } from 'child_process';
import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
import { expandPath } from './file_utils';
export function registerShellHandlers() {
ipcMain.handle('native:open-folder', async (event, folderPath: string) => {
const cleanPath = expandPath(folderPath);
const error = await shell.openPath(cleanPath);
return { success: !error, error };
});
ipcMain.handle('native:run-cmd', async (event, { cmd, timeout = 30000 }) => {
const cleanCmd = expandPath(cmd);
return new Promise((resolve) => {
exec(cleanCmd, { timeout }, (error, stdout, stderr) => {
resolve({ success: !error, stdout: stdout.trim(), stderr: stderr.trim(), error: error ? error.message : null });
});
});
});
ipcMain.handle('native:run-cmd-sync', async (event, { cmd }) => {
const cleanCmd = expandPath(cmd);
try {
const stdout = execSync(cleanCmd).toString();
return { success: true, stdout: stdout.trim() };
} catch (error: any) {
return { success: false, error: error.message, stderr: error.stderr?.toString() };
}
});
ipcMain.handle('native:run-osascript', async (event, script: string) => {
if (os.platform() !== 'darwin') return { success: false, error: 'AppleScript is only available on macOS' };
// HARDENED: Write script to a temp .scpt file rather than passing inline via -e.
// The old -e approach (`osascript -e "..."`) has two fatal flaws:
// 1. It breaks on multi-line scripts.
// 2. It breaks on paths containing spaces or special characters (quotes, parens, etc.)
// Writing to a file sidesteps both — no shell escaping needed at all.
// The .scpt file is deleted immediately after execution (success or failure).
// Worst case on crash: a stale .scpt in /tmp, cleared on next OS reboot.
//
// LEGACY (removed): const cmd = `osascript -e "${script.replace(/"/g, '\\"')}"`;
const tmp_script_path = path.join(os.tmpdir(), `ae_osa_${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}"`, (error, stdout, stderr) => {
try { fs.unlinkSync(tmp_script_path); } catch {}
resolve({ success: !error, stdout: stdout.trim(), stderr: stderr.trim(), error: error ? error.message : null });
});
});
});
ipcMain.handle('native:kill-processes', async (event, { process_name_li = [] }) => {
console.log(`Native: Killing processes -> `, process_name_li);
const results = [];
for (const name of process_name_li) {
const cmd = os.platform() === 'win32'
? `taskkill /F /IM ${name} /T`
: `pkill -f ${name}`;
try {
execSync(cmd);
results.push({ name, success: true });
} catch (e: any) {
results.push({ name, success: false, error: e.message });
}
}
return { success: true, results };
});
ipcMain.handle('native:open-local-file-v2', async (event, filePath: string) => {
const cleanPath = expandPath(filePath);
const error = await shell.openPath(cleanPath);
return { success: !error, error };
});
ipcMain.handle('native:launch-presentation', async (event, { path: rawPath, app: appType = 'default' }) => {
const cleanedPath = expandPath(rawPath);
console.log(`Native: Launching Presentation -> ${cleanedPath} (App: ${appType})`);
if (os.platform() === 'linux') {
const cmd = `libreoffice --impress "${cleanedPath}"`;
return new Promise((resolve) => {
exec(cmd, (err, stdout, stderr) => {
if (err) resolve({ success: false, error: err.message });
else resolve({ success: true, stdout, stderr });
});
});
}
if (os.platform() === 'darwin') {
let script = '';
if (appType === 'keynote') {
script = `
tell application "Keynote"
activate
open (POSIX file "${cleanedPath}")
delay 1
start (front document)
end tell
`.trim();
} else if (appType === 'powerpoint') {
script = `
tell application "Microsoft PowerPoint"
activate
open (POSIX file "${cleanedPath}")
delay 1
run slide show of active presentation
end tell
`.trim();
}
if (script) {
const tmp_script_path = path.join(os.tmpdir(), `ae_launch_${Date.now()}.scpt`);
return new Promise((resolve) => {
try {
fs.writeFileSync(tmp_script_path, script);
} 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 });
});
});
}
}
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: {}
}
];
});
}

246
src/main/system_handlers.ts Normal file
View File

@@ -0,0 +1,246 @@
import { ipcMain, BrowserWindow, app, shell } from 'electron';
import * as os from 'os';
import * as path from 'path';
import * as fs from 'fs';
import { exec, spawn } from 'child_process';
import axios from 'axios';
import { expandPath } from './file_utils';
// Helper to execute shell commands
const runExec = (cmd: string): Promise<{ success: boolean; stdout?: string; stderr?: string; error?: string }> => {
return new Promise((resolve) => {
exec(cmd, (error, stdout, stderr) => {
resolve({
success: !error,
stdout: stdout.trim(),
stderr: stderr.trim(),
error: error ? error.message : undefined
});
});
});
};
let recordingProcess: any = null;
export function registerSystemHandlers() {
// 1. Window Control
ipcMain.handle('native:window-control', async (event, { action, value }) => {
const win = BrowserWindow.fromWebContents(event.sender);
if (!win) return { success: false, error: 'No window found' };
switch (action) {
case 'maximize': win.maximize(); break;
case 'unmaximize': win.unmaximize(); break;
case 'minimize': win.minimize(); break;
case 'restore': win.restore(); break;
case 'close': win.close(); break;
case 'devtools':
if (value) win.webContents.openDevTools();
else win.webContents.closeDevTools();
break;
case 'kiosk': win.setKiosk(!!value); break;
case 'fullscreen': win.setFullScreen(!!value); break;
case 'reload': win.reload(); break;
default: return { success: false, error: `Unknown action: ${action}` };
}
return { success: true };
});
// 2. Set Wallpaper
ipcMain.handle('native:set-wallpaper', async (event, { path: imagePath }) => {
const cleanPath = expandPath(imagePath);
if (!fs.existsSync(cleanPath)) return { success: false, error: 'Image file not found' };
if (os.platform() === 'darwin') {
const script = `tell application "System Events" to set picture of every desktop to "${cleanPath}"`;
return await runExec(`osascript -e '${script}'`);
} else if (os.platform() === 'linux') {
// Gnome/Ubuntu default
return await runExec(`gsettings set org.gnome.desktop.background picture-uri "file://${cleanPath}"`);
}
return { success: false, error: 'Platform not supported' };
});
// 3. Power Control
ipcMain.handle('native:power-control', async (event, { action }) => {
let cmd = '';
const isMac = os.platform() === 'darwin';
const isLinux = os.platform() === 'linux';
if (action === 'shutdown') {
if (isMac) cmd = 'shutdown -h now'; // Requires sudo/admin usually
if (isLinux) cmd = 'shutdown -h now';
} else if (action === 'reboot') {
if (isMac) cmd = 'shutdown -r now';
if (isLinux) cmd = 'reboot';
} else if (action === 'sleep') {
if (isMac) cmd = 'pmset sleepnow';
if (isLinux) cmd = 'systemctl suspend';
}
if (!cmd) return { success: false, error: 'Action not supported' };
// NOTE: These commands often require root.
// For a kiosk, you might configure sudoers to allow this specific command without password.
return await runExec(cmd);
});
// 4. Open External (Browser)
ipcMain.handle('native:open-external', async (event, { url, app: appName }) => {
if (appName === 'chrome') {
if (os.platform() === 'darwin') {
return await runExec(`open -a "Google Chrome" "${url}"`);
} else if (os.platform() === 'linux') {
return await runExec(`google-chrome "${url}"`);
}
} else if (appName === 'firefox') {
if (os.platform() === 'darwin') {
return await runExec(`open -a "Firefox" "${url}"`);
} else if (os.platform() === 'linux') {
return await runExec(`firefox "${url}"`);
}
}
// Default system handler
await shell.openExternal(url);
return { success: true };
});
// 5. Manage Recording (Aperture Wrapper)
ipcMain.handle('native:manage-recording', async (event, { action, options }) => {
if (os.platform() !== 'darwin') return { success: false, error: 'Recording only supported on macOS' };
// Path to bundled aperture binary
// In dev: ./resources/bin/aperture
// In prod: process.resourcesPath/bin/aperture
const binPath = app.isPackaged
? path.join(process.resourcesPath, 'bin', 'aperture')
: path.join(__dirname, '../../resources/bin/aperture'); // Adjust based on structure
if (action === 'start') {
if (recordingProcess) return { success: false, error: 'Recording already in progress' };
const { fps = 30, audioDeviceId, output } = options || {};
const cleanOutput = expandPath(output || '~/tmp/recording.mp4');
const args = ['run', '--fps', fps, '--output', cleanOutput];
if (audioDeviceId) args.push('--audio-device-id', audioDeviceId);
// Spawn process
// Note: aperture is a CLI tool. We might need 'aperture' node package or the binary.
// Assuming binary usage here.
try {
console.log(`Starting recording: ${binPath} ${args.join(' ')}`);
recordingProcess = spawn(binPath, args);
recordingProcess.on('error', (err: any) => {
console.error('Recording error:', err);
recordingProcess = null;
});
recordingProcess.on('exit', (code: any) => {
console.log(`Recording exited with code ${code}`);
recordingProcess = null;
});
return { success: true, pid: recordingProcess.pid };
} catch (e: any) {
return { success: false, error: e.message };
}
} else if (action === 'stop') {
if (!recordingProcess) return { success: false, error: 'No recording in progress' };
recordingProcess.kill('SIGINT'); // Send interrupt to stop cleanly
recordingProcess = null;
return { success: true };
} else if (action === 'status') {
return { success: true, isRecording: !!recordingProcess };
}
return { success: false, error: 'Unknown action' };
});
// 6. Set Display Layout (Displayplacer)
ipcMain.handle('native:set-display-layout', async (event, { mode, configStr }) => {
if (os.platform() !== 'darwin') return { success: false, error: 'Display control only supported on macOS' };
const binPath = app.isPackaged
? path.join(process.resourcesPath, 'bin', 'displayplacer')
: path.join(__dirname, '../../resources/bin/displayplacer');
let cmd = '';
if (mode === 'mirror') {
// This usually requires querying current IDs, which is complex.
// If configStr is provided (output of 'displayplacer list'), use it.
if (configStr) {
cmd = `"${binPath}" ${configStr}`;
} else {
return { success: false, error: 'Config string required for now' };
}
} else if (mode === 'extend') {
if (configStr) {
cmd = `"${binPath}" ${configStr}`;
}
}
if (cmd) {
return await runExec(cmd);
}
return { success: false, error: 'Invalid mode or missing config' };
});
// 7. Update App
ipcMain.handle('native:update-app', async (event, { source, url, path: localPath }) => {
// 1. Determine Source File
let updateFile = '';
const tempDir = os.tmpdir();
const destName = 'update_package.zip'; // Or .app, .AppImage
const destPath = path.join(tempDir, destName);
if (source === 'url' && url) {
// Download
try {
const response = await axios({
method: 'get',
url: url,
responseType: 'stream'
});
const writer = fs.createWriteStream(destPath);
response.data.pipe(writer);
await new Promise((resolve, reject) => {
writer.on('finish', () => resolve(true));
writer.on('error', reject);
});
updateFile = destPath;
} catch (e: any) {
return { success: false, error: `Download failed: ${e.message}` };
}
} else if (source === 'file' && localPath) {
const cleanLocal = expandPath(localPath);
if (fs.existsSync(cleanLocal)) {
updateFile = cleanLocal;
} else {
return { success: false, error: 'Local update file not found' };
}
}
if (!updateFile) return { success: false, error: 'No update source provided' };
// 2. Install Logic (Stub)
// Real implementation depends on OS and packaging format.
// macOS: Mount DMG, copy .app to /Applications? Or Unzip .app?
// Linux: chmod +x AppImage and move?
console.log(`Ready to install update from: ${updateFile}`);
// For now, just return success so the UI knows we "downloaded" it.
return { success: true, message: 'Update downloaded/located. Installation logic requires packaging specifics.', downloadedPath: updateFile };
});
}

32
src/preload/index.ts Normal file
View File

@@ -0,0 +1,32 @@
import { contextBridge, ipcRenderer } from 'electron';
contextBridge.exposeInMainWorld('aetherNative', {
get_seed_config: () => ipcRenderer.invoke('get-seed-config'),
get_device_config: () => ipcRenderer.invoke('get-device-config'),
get_jwt: () => ipcRenderer.invoke('get-jwt'),
get_device_info: () => ipcRenderer.invoke('get-device-info'),
open_folder: (path: string) => ipcRenderer.invoke('native:open-folder', path),
run_cmd: (args: any) => ipcRenderer.invoke('native:run-cmd', args),
run_cmd_sync: (args: any) => ipcRenderer.invoke('native:run-cmd-sync', args),
run_osascript: (script: string) => ipcRenderer.invoke('native:run-osascript', script),
kill_processes: (args: any) => ipcRenderer.invoke('native:kill-processes', args),
open_local_file_v2: (path: string) => ipcRenderer.invoke('native:open-local-file-v2', path),
check_cache: (args: any) => ipcRenderer.invoke('native:check-cache', args),
download_to_cache: (args: any) => ipcRenderer.invoke('native:download-to-cache', args),
copy_from_cache_to_temp: (args: any) => ipcRenderer.invoke('native:copy-from-cache-to-temp', 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'),
// System Handlers (V5)
set_wallpaper: (args: any) => ipcRenderer.invoke('native:set-wallpaper', args),
update_app: (args: any) => ipcRenderer.invoke('native:update-app', args),
window_control: (args: any) => ipcRenderer.invoke('native:window-control', args),
manage_recording: (args: any) => ipcRenderer.invoke('native:manage-recording', args),
set_display_layout: (args: any) => ipcRenderer.invoke('native:set-display-layout', args),
power_control: (args: any) => ipcRenderer.invoke('native:power-control', args),
open_external: (args: any) => ipcRenderer.invoke('native:open-external', args),
});

50
src/shared/types.ts Normal file
View File

@@ -0,0 +1,50 @@
export interface SeedConfig {
event_device_id: string;
primary_api_base_url: string;
backup_api_base_url: string | null;
onsite_api_base_url: string | null;
aether_api_key: string;
}
export interface AetherNativeBridge {
get_seed_config: () => Promise<SeedConfig | null>;
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}>;
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
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, 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}>;
control_presentation: (args: {app: 'powerpoint' | 'keynote', action: 'next' | 'prev' | 'start' | 'stop'}) => Promise<{success: boolean, error?: string, stdout?: string, stderr?: string}>;
// System Handlers (Phase 5)
window_control: (args: {action: 'maximize' | 'unmaximize' | 'minimize' | 'restore' | 'close' | 'devtools' | 'kiosk' | 'fullscreen' | 'reload', value?: boolean}) => Promise<{success: boolean, error?: string}>;
set_wallpaper: (args: {path: string}) => Promise<{success: boolean, error?: string, stdout?: string, stderr?: string}>;
power_control: (args: {action: 'shutdown' | 'reboot' | 'sleep'}) => Promise<{success: boolean, error?: string}>;
open_external: (args: {url: string, app?: 'chrome' | 'firefox'}) => Promise<{success: boolean, error?: string}>;
manage_recording: (args: {action: 'start' | 'stop' | 'status', options?: {fps?: number, audioDeviceId?: string, output?: string}}) => Promise<{success: boolean, isRecording?: boolean, pid?: number, error?: string}>;
set_display_layout: (args: {mode: 'mirror' | 'extend', configStr?: string | null}) => Promise<{success: boolean, error?: string, stdout?: string, stderr?: string}>;
update_app: (args: {source: 'url' | 'file', url?: string, path?: string}) => Promise<{success: boolean, message?: string, downloadedPath?: string, error?: string}>;
// Self-Documentation
list_tools: () => Promise<Array<{name: string, description: string, params: object}>>;
}
declare global {
interface Window {
aetherNative: AetherNativeBridge;
}
}

View File

@@ -0,0 +1,90 @@
import requests
import json
def test_bootstrap(device_id, api_key, base_url='https://dev-api.oneskyit.com'):
"""
Replicates the two-step bootstrap sequence in api_client.ts:
Step 1: GET event_device → extract account_id + app_base_url (fqdn)
Step 2: POST site_domain/search → returns the correct site context
"""
print(f'\n=== Bootstrap test ===')
print(f'Device: {device_id}')
print(f'Base URL: {base_url}')
# --- Step 1: Get Device Config ---
device_url = f'{base_url}/v3/crud/event_device/{device_id}'
headers_step1 = {
'Content-Type': 'application/json',
'x-aether-api-key': api_key,
'x-no-account-id': 'bypass',
}
print(f'\n-- Step 1: GET {device_url}')
try:
r1 = requests.get(device_url, headers=headers_step1)
print(f' Status: {r1.status_code}')
if r1.status_code != 200:
print(f' Error: {r1.text}')
return
device_data = r1.json().get('data', {})
important_fields = [
'account_id', 'app_base_url', 'code', 'name',
'event_id', 'event_location_id',
'local_file_cache_path', 'host_file_temp_path', 'recording_path',
]
for field in important_fields:
print(f' {field}: {device_data.get(field, "MISSING")}')
account_id = device_data.get('account_id') or device_data.get('account_id_random')
fqdn = device_data.get('app_base_url', 'native-demo.oneskyit.com')
except Exception as e:
print(f' Request failed: {e}')
return
# --- Step 2: Get Site Context ---
search_url = f'{base_url}/v3/crud/site_domain/search?limit=1'
headers_step2 = {
'Content-Type': 'application/json',
'x-aether-api-key': api_key,
'x-account-id': account_id or '',
}
body = {
'and': [{'field': 'fqdn', 'op': 'eq', 'value': fqdn}]
}
print(f'\n-- Step 2: POST {search_url}')
print(f' Searching for fqdn: {fqdn}')
try:
r2 = requests.post(search_url, headers=headers_step2, json=body)
print(f' Status: {r2.status_code}')
if r2.status_code != 200:
print(f' Error: {r2.text}')
return
results = r2.json().get('data', [])
print(f' Results returned: {len(results)}')
if results:
sd = results[0]
print(f' fqdn: {sd.get("fqdn")}')
print(f' account_id: {sd.get("account_id")}')
print(f' site_id: {sd.get("site_id")}')
if sd.get('account_id') != account_id:
print(f' WARNING: site_domain account_id does not match device account_id!')
else:
print(f' OK: account_id matches device.')
else:
print(f' WARNING: No site_domain found for fqdn "{fqdn}"')
except Exception as e:
print(f' Request failed: {e}')
if __name__ == '__main__':
# Dev device (dev-api)
test_bootstrap(
device_id='dbgMWS3KEHE',
api_key='INSdG85ANwsEIru3nUttMw',
base_url='https://dev-api.oneskyit.com',
)

21
tsconfig.json Normal file
View File

@@ -0,0 +1,21 @@
{
"compilerOptions": {
"target": "ESNext",
"module": "CommonJS",
"lib": ["ESNext", "DOM"],
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"moduleResolution": "node",
"outDir": "dist",
"rootDir": "src",
"sourceMap": true,
"baseUrl": ".",
"paths": {
"@shared/*": ["src/shared/*"]
}
},
"include": ["src/**/*"],
"exclude": ["node_modules"]
}