From 196f5559385c5a809438f98ae5478d646221b1f2 Mon Sep 17 00:00:00 2001 From: Patrick Roza Date: Sat, 25 Jul 2026 10:27:04 +0200 Subject: [PATCH] fix(desktop): avoid plutil for custom open-with apps --- .../desktop/src/shell/DesktopOpenWith.test.ts | 30 -------- apps/desktop/src/shell/DesktopOpenWith.ts | 68 ++----------------- packages/contracts/src/openWith.ts | 22 ------ 3 files changed, 4 insertions(+), 116 deletions(-) diff --git a/apps/desktop/src/shell/DesktopOpenWith.test.ts b/apps/desktop/src/shell/DesktopOpenWith.test.ts index 286a7f161a8..e98b202a025 100644 --- a/apps/desktop/src/shell/DesktopOpenWith.test.ts +++ b/apps/desktop/src/shell/DesktopOpenWith.test.ts @@ -9,7 +9,6 @@ import { } from "@t3tools/contracts"; import * as Effect from "effect/Effect"; import * as FileSystem from "effect/FileSystem"; -import * as Path from "effect/Path"; import * as Schema from "effect/Schema"; import * as Layer from "effect/Layer"; import * as Option from "effect/Option"; @@ -144,35 +143,6 @@ describe("DesktopOpenWith launch resolution", () => { }).pipe(Effect.provide(NodeServices.layer)), ); - it.effect("resolves CFBundleExecutable and reports missing bundle executables", () => - Effect.gen(function* () { - // oxlint-disable-next-line t3code/no-global-process-runtime -- Platform-gated native fixture. - if (process.platform !== "darwin") return; - const fileSystem = yield* FileSystem.FileSystem; - const path = yield* Path.Path; - const base = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-open-with-test-" }); - const applicationPath = path.join(base, "Fixture.app"); - const contentsPath = path.join(applicationPath, "Contents"); - const executableDirectory = path.join(contentsPath, "MacOS"); - yield* fileSystem.makeDirectory(executableDirectory, { recursive: true }); - yield* fileSystem.writeFileString( - path.join(contentsPath, "Info.plist"), - ` - CFBundleExecutableFixture`, - ); - const executablePath = path.join(executableDirectory, "Fixture"); - yield* fileSystem.writeFileString(executablePath, "#!/bin/sh\n"); - - assert.equal( - yield* DesktopOpenWith.resolveMacBundleExecutable(applicationPath), - executablePath, - ); - yield* fileSystem.remove(executablePath); - const error = yield* Effect.flip(DesktopOpenWith.resolveMacBundleExecutable(applicationPath)); - assert.equal(error.reason, "missing-executable"); - }).pipe(Effect.provide(NodeServices.layer), Effect.scoped), - ); - it.effect("reports available and missing command presentations", () => Effect.gen(function* () { const openWith = yield* DesktopOpenWith.DesktopOpenWith; diff --git a/apps/desktop/src/shell/DesktopOpenWith.ts b/apps/desktop/src/shell/DesktopOpenWith.ts index 464d71fce48..73452ba9610 100644 --- a/apps/desktop/src/shell/DesktopOpenWith.ts +++ b/apps/desktop/src/shell/DesktopOpenWith.ts @@ -1,6 +1,5 @@ // @effect-diagnostics nodeBuiltinImport:off - macOS bundle paths use the host path grammar. import { - OpenWithBundleResolutionError, OpenWithEnvironmentError, OpenWithInvalidTargetError, OpenWithMissingEntryError, @@ -70,65 +69,6 @@ const entryIsAvailable = Effect.fn("desktop.openWith.entryIsAvailable")(function return Option.isSome(stat) && stat.value.type === "Directory"; }); -export const resolveMacBundleExecutable = Effect.fn("desktop.openWith.resolveMacBundleExecutable")( - function* (applicationPath: string) { - if (!isMacApplicationPath(applicationPath)) { - return yield* new OpenWithBundleResolutionError({ - applicationPath, - reason: "invalid-application-path", - }); - } - const infoPlistPath = NodePath.join(applicationPath, "Contents", "Info.plist"); - const plistStat = yield* statPath(infoPlistPath); - if (Option.isNone(plistStat) || plistStat.value.type !== "File") { - return yield* new OpenWithBundleResolutionError({ - applicationPath, - reason: "missing-info-plist", - }); - } - - const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; - const executableName = yield* spawner - .string( - ChildProcess.make( - "/usr/bin/plutil", - ["-extract", "CFBundleExecutable", "raw", "-o", "-", infoPlistPath], - { stdin: "ignore", stdout: "pipe", stderr: "pipe" }, - ), - ) - .pipe( - Effect.map((output) => output.trim()), - Effect.mapError( - (cause) => - new OpenWithBundleResolutionError({ - applicationPath, - reason: "malformed-info-plist", - cause, - }), - ), - ); - if ( - executableName.length === 0 || - executableName.includes("/") || - executableName.includes("\\") - ) { - return yield* new OpenWithBundleResolutionError({ - applicationPath, - reason: "malformed-info-plist", - }); - } - const executablePath = NodePath.join(applicationPath, "Contents", "MacOS", executableName); - const executableStat = yield* statPath(executablePath); - if (Option.isNone(executableStat) || executableStat.value.type !== "File") { - return yield* new OpenWithBundleResolutionError({ - applicationPath, - reason: "missing-executable", - }); - } - return executablePath; - }, -); - const expandDirectoryArguments = (args: readonly string[], directory: string): string[] => args.map((argument) => argument.replaceAll("{directory}", directory)); @@ -169,8 +109,8 @@ export const resolveOpenWithLaunch = Effect.fn("desktop.openWith.resolveLaunch") if (entry.directoryMode === "working-directory") { if (entry.invocation.type === "mac-application") { return { - command: yield* resolveMacBundleExecutable(entry.invocation.applicationPath), - args: [...entry.arguments], + command: "/usr/bin/open", + args: ["-a", entry.invocation.applicationPath, "--args", ...entry.arguments], cwd: directory, }; } @@ -187,8 +127,8 @@ export const resolveOpenWithLaunch = Effect.fn("desktop.openWith.resolveLaunch") const args = expandDirectoryArguments(entry.arguments, directory); if (entry.invocation.type === "mac-application") { return { - command: yield* resolveMacBundleExecutable(entry.invocation.applicationPath), - args, + command: "/usr/bin/open", + args: ["-a", entry.invocation.applicationPath, "--args", ...args], cwd: null, }; } diff --git a/packages/contracts/src/openWith.ts b/packages/contracts/src/openWith.ts index 9df4e1db990..94e40032a51 100644 --- a/packages/contracts/src/openWith.ts +++ b/packages/contracts/src/openWith.ts @@ -132,27 +132,6 @@ export class OpenWithUnavailableApplicationError extends Schema.TaggedErrorClass } } -export const OpenWithBundleResolutionReason = Schema.Literals([ - "invalid-application-path", - "missing-info-plist", - "malformed-info-plist", - "missing-executable", -]); -export type OpenWithBundleResolutionReason = typeof OpenWithBundleResolutionReason.Type; - -export class OpenWithBundleResolutionError extends Schema.TaggedErrorClass()( - "OpenWithBundleResolutionError", - { - applicationPath: Schema.String, - reason: OpenWithBundleResolutionReason, - cause: Schema.optional(Schema.Defect()), - }, -) { - override get message(): string { - return `Unable to resolve the executable in macOS application '${this.applicationPath}' (${this.reason}).`; - } -} - export class OpenWithSpawnError extends Schema.TaggedErrorClass()( "OpenWithSpawnError", { @@ -173,7 +152,6 @@ export const OpenWithLaunchError = Schema.Union([ OpenWithMissingEntryError, OpenWithInvalidTargetError, OpenWithUnavailableApplicationError, - OpenWithBundleResolutionError, OpenWithSpawnError, ]); export type OpenWithLaunchError = typeof OpenWithLaunchError.Type;