feat(native): open iOS-style and iOS-on-Mac wrapper app bundles

Every native command rejected an iOS app installed on an Apple silicon
Mac, such as /Applications/Stash.app, with "app has no readable
CFBundleExecutable": the resolver understood only the macOS layout,
Contents/Info.plist with Contents/MacOS/<executable>. iOS-style bundles,
including those extracted from an IPA, keep Info.plist and the program
file at the bundle root, and an iOS-on-Mac wrapper holds that bundle in
Wrapper/<Name>.app.

Bundle resolution moves to AppBundleExecutable and accepts the macOS
layout, then the flat layout, then a wrapper's single real Wrapper/*.app
directory. The WrappedBundle symlink is never followed, and the program
file must stay inside its bundle. inspect_plist defaults to the Info.plist
beside the program file when the bundle has no Contents/Info.plist.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Kaoru0822andClaude Opus 5.5 committed 2026-10-07 08:59:28 +00:00
1 parent eab34a9d8c
commit aeebba391e
5 files changed
+266 -82

No files matched your search

+18 -3
View File
@@ -713,6 +713,14 @@ const requireRegularPlist = async (
}
};
const isFile = async (path: string): Promise<boolean> => {
try {
return (await stat(path)).isFile();
} catch {
return false;
}
};
const resolvePlistPath = async (
target: BinaryTarget,
requested: string | undefined,
@@ -723,10 +731,17 @@ const resolvePlistPath = async (
return ok(resolve(requested));
const sourceMetadata = await stat(source);
const root = sourceMetadata.isDirectory() ? source : dirname(target.path);
if (requested !== undefined) return ok(resolve(root, requested));
const macOS = resolve(root, "Contents/Info.plist");
// iOS-style bundles, including the one inside an iOS-on-Mac wrapper, keep
// Info.plist beside the program file the target resolved to.
const beside = resolve(dirname(target.path), "Info.plist");
return ok(
requested === undefined
? resolve(root, "Contents/Info.plist")
: resolve(root, requested),
sourceMetadata.isDirectory() &&
!(await isFile(macOS)) &&
(await isFile(beside))
? beside
: macOS,
);
} catch (cause: unknown) {
return err(