mirror of
https://github.com/morluto/rea.git
synced 2026-11-10 08:47:00 +00:00
* fix(doctor): check only the current platform's Hopper launcher candidates Doctor built one launcher list for every platform, so on macOS it could report a Linux-only path as a healthy Hopper ahead of a renamed bundle, and setup then registered that path. Give per-platform automatic candidates one owner that runtime configuration and doctor both read, scan renamed macOS bundles and probe the Homebrew cask only on macOS, and share runtime's renamed-bundle scan with the doctor host. Fixes #6228 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(doctor): keep the renamed Hopper bundle scan asynchronous Sharing runtime's synchronous scan made the doctor host read ~/Applications with readdirSync, which blocks the event loop on a slow or network-mounted home. Give MacHopperPaths an asynchronous scan for doctor beside runtime's lazy synchronous one, sharing the directory filter and candidate builder, and check that both return the same candidates. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(doctor): model renamed launchers with runtime's POSIX joins Since #5235, runtime builds macOS launcher paths with posix.join, so on Windows the discovery table's native-join renamed launcher no longer matched runtime's candidate. Model launchers with posix.join and keep filesystem setup native, as MacHopperPaths.test.ts does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
98 lines
3.2 KiB
TypeScript
98 lines
3.2 KiB
TypeScript
import { readdirSync, type Dirent } from "node:fs";
|
|
import { readdir } from "node:fs/promises";
|
|
import { posix } from "node:path";
|
|
|
|
export const MAC_HOPPER_APPLICATION_DIRECTORY = "/Applications";
|
|
export const MAC_HOPPER_APPLICATION_NAME = "Hopper Disassembler.app";
|
|
export const MAC_HOPPER_USER_APPLICATIONS_DIRECTORY = "Applications";
|
|
|
|
export const MAC_HOPPER_SYSTEM_LAUNCHER = posix.join(
|
|
MAC_HOPPER_APPLICATION_DIRECTORY,
|
|
MAC_HOPPER_APPLICATION_NAME,
|
|
"Contents/MacOS/hopper",
|
|
);
|
|
|
|
export const macHopperUserApplicationPath = (homeDirectory: string): string =>
|
|
posix.join(
|
|
homeDirectory,
|
|
MAC_HOPPER_USER_APPLICATIONS_DIRECTORY,
|
|
MAC_HOPPER_APPLICATION_NAME,
|
|
);
|
|
|
|
export const macHopperApplicationRoots = (
|
|
homeDirectory: string,
|
|
): readonly string[] => [
|
|
MAC_HOPPER_APPLICATION_DIRECTORY,
|
|
posix.join(homeDirectory, MAC_HOPPER_USER_APPLICATIONS_DIRECTORY),
|
|
];
|
|
|
|
export const macHopperCanonicalLauncherCandidates = (
|
|
homeDirectory: string,
|
|
): readonly string[] => [
|
|
MAC_HOPPER_SYSTEM_LAUNCHER,
|
|
posix.join(
|
|
macHopperUserApplicationPath(homeDirectory),
|
|
"Contents/MacOS/hopper",
|
|
),
|
|
];
|
|
|
|
/** Build deterministic candidates from the discovered app bundle names. */
|
|
export const macHopperRenamedLauncherCandidates = (
|
|
homeDirectory: string,
|
|
bundleNamesByRoot: ReadonlyMap<string, readonly string[]>,
|
|
): readonly string[] =>
|
|
macHopperApplicationRoots(homeDirectory).flatMap((root) =>
|
|
[...new Set(bundleNamesByRoot.get(root) ?? [])]
|
|
.filter(
|
|
(name) =>
|
|
/^Hopper.*\.app$/iu.test(name) &&
|
|
name.toLocaleLowerCase("en-US") !==
|
|
MAC_HOPPER_APPLICATION_NAME.toLocaleLowerCase("en-US"),
|
|
)
|
|
.sort()
|
|
.map((name) => posix.join(root, name, "Contents/MacOS/hopper")),
|
|
);
|
|
|
|
const directoryNames = (entries: readonly Dirent[]): readonly string[] =>
|
|
entries.filter((entry) => entry.isDirectory()).map(({ name }) => name);
|
|
|
|
const localBundleNames = (root: string): readonly string[] => {
|
|
try {
|
|
return directoryNames(readdirSync(root, { withFileTypes: true }));
|
|
} catch (cause: unknown) {
|
|
// Missing or unreadable optional app directories contain no candidates.
|
|
void cause;
|
|
return [];
|
|
}
|
|
};
|
|
|
|
/** Yield renamed bundles from the local filesystem, reading each root only if needed. */
|
|
export function* localMacHopperRenamedLauncherCandidates(
|
|
homeDirectory: string,
|
|
): Generator<string> {
|
|
for (const root of macHopperApplicationRoots(homeDirectory))
|
|
yield* macHopperRenamedLauncherCandidates(
|
|
homeDirectory,
|
|
new Map([[root, localBundleNames(root)]]),
|
|
);
|
|
}
|
|
|
|
/** Read the same renamed bundles without blocking the event loop, for doctor. */
|
|
export const readMacHopperRenamedLauncherCandidates = async (
|
|
homeDirectory: string,
|
|
): Promise<readonly string[]> => {
|
|
const bundleNamesByRoot = new Map<string, readonly string[]>();
|
|
for (const root of macHopperApplicationRoots(homeDirectory)) {
|
|
try {
|
|
bundleNamesByRoot.set(
|
|
root,
|
|
directoryNames(await readdir(root, { withFileTypes: true })),
|
|
);
|
|
} catch (cause: unknown) {
|
|
// Missing or unreadable optional app directories contain no candidates.
|
|
void cause;
|
|
}
|
|
}
|
|
return macHopperRenamedLauncherCandidates(homeDirectory, bundleNamesByRoot);
|
|
};
|