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>
This commit is contained in:
Scott Idem
2026-05-11 16:48:15 -04:00
parent 36aed19169
commit bab08cd8a7
14 changed files with 797 additions and 1741 deletions

View File

@@ -100,8 +100,7 @@ function registerFileHandlers() {
method: 'get', url, responseType: 'stream',
headers: {
'x-aether-api-key': api_key,
'x-account-id': account_id || '',
'x-no-account-id': 'Nothing to See Here'
'x-account-id': account_id || ''
}
});
const writer = fs.createWriteStream(tmp_path);
@@ -131,7 +130,7 @@ function registerFileHandlers() {
endpoints_in_progress = endpoints_in_progress.filter(e => e !== url);
}
});
electron_1.ipcMain.handle('native:launch-from-cache', async (event, { cache_root, hash, temp_root, filename, hash_prefix_length = 2 }) => {
electron_1.ipcMain.handle('native:launch-from-cache', async (event, { cache_root, hash, temp_root, filename, hash_prefix_length = 2, script_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);
@@ -141,10 +140,52 @@ function registerFileHandlers() {
fs.mkdirSync(expanded_temp, { recursive: true });
// 1. Copy the file to temp folder with original name
fs.copyFileSync(source, target);
// 2. Determine file type
// 2a. Data-driven script override (no rebuild needed for script changes).
// Set via event_device.data_json.launch_scripts or $events_loc.launcher.launch_scripts.
// Format: AppleScript string with {{path}} placeholder, OR "shell:<cmd> {{path}}"
if (script_template) {
const resolved = script_template.replace(/\{\{path\}\}/g, target);
if (resolved.startsWith('shell:')) {
const cmd = resolved.slice(6).trim();
console.log(`Native: Running custom shell script for ${filename}`);
return new Promise((resolve_fn) => {
(0, child_process_1.exec)(cmd, (err, stdout, stderr) => {
if (err)
resolve_fn({ success: false, error: err.message, stderr: stderr.trim() });
else
resolve_fn({ success: true, stdout: stdout.trim() });
});
});
}
else {
// Treat as AppleScript — write to temp .scpt file (same hardened approach used below)
console.log(`Native: Running custom AppleScript for ${filename}`);
const tmp_script_path = path.join(os.tmpdir(), `ae_launch_${Date.now()}.scpt`);
return new Promise((resolve_fn) => {
try {
fs.writeFileSync(tmp_script_path, resolved.trim());
}
catch (e) {
resolve_fn({ success: false, error: `Failed to write AppleScript temp file: ${e.message}` });
return;
}
(0, child_process_1.exec)(`osascript "${tmp_script_path}"`, (err) => {
try {
fs.unlinkSync(tmp_script_path);
}
catch { }
if (err)
resolve_fn({ success: false, error: err.message });
else
resolve_fn({ success: true });
});
});
}
}
// 2b. Determine file type (legacy hardcoded launch logic — used when no script_template provided)
const ext = path.extname(filename).toLowerCase().replace('.', '');
const is_pres = ['pptx', 'ppt', 'key', 'pdf', 'odp'].includes(ext);
// 3. Optimized Launch (LibreOffice / AppleScript)
// 3. Hardcoded launch (legacy — still the default when no script_template is configured)
if (is_pres) {
if (os.platform() === 'linux') {
console.log(`Native: Launching LibreOffice (--impress) for ${target}`);
@@ -216,5 +257,30 @@ function registerFileHandlers() {
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 the built-in hardcoded logic is sufficient; 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