mirror of
https://github.com/morluto/rea.git
synced 2026-11-08 15:56:35 +00:00
`rea doctor`, documented as strictly read-only, ran `brew --prefix --cask hopper-disassembler` when Hopper was not in a standard location. Current Homebrew rejects that option combination, but any brew command naming a cask first fetches the cask API, so each doctor run wrote about 600 files (36 MB) into the user's Homebrew cache. Ask brew only for its Caskroom, which brew answers without loading the cask API, and read the application directory Homebrew recorded in the Hopper cask's install receipt. Fixes #1165 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
78 lines
2.2 KiB
TypeScript
78 lines
2.2 KiB
TypeScript
import { readFile } from "node:fs/promises";
|
|
import { join } from "node:path";
|
|
|
|
import { z } from "zod";
|
|
|
|
/** Standard executable names and locations used to discover Homebrew. */
|
|
const HOMEBREW_COMMANDS = [
|
|
"brew",
|
|
"/opt/homebrew/bin/brew",
|
|
"/usr/local/bin/brew",
|
|
] as const;
|
|
|
|
/**
|
|
* Return the first defined result from standard Homebrew executables.
|
|
* A probe returning `undefined` means that location was unavailable or failed;
|
|
* other falsy values are successful results and stop discovery.
|
|
*/
|
|
export const probeHomebrew = async <T>(
|
|
probe: (command: string) => Promise<T | undefined>,
|
|
): Promise<T | undefined> => {
|
|
for (const command of HOMEBREW_COMMANDS) {
|
|
const result = await probe(command);
|
|
if (result !== undefined) return result;
|
|
}
|
|
return undefined;
|
|
};
|
|
|
|
const caskDirectoriesSchema = z
|
|
.object({ appdir: z.string().min(1).optional() })
|
|
.loose();
|
|
const caskConfigSchema = z
|
|
.object({
|
|
default: caskDirectoriesSchema.optional(),
|
|
env: caskDirectoriesSchema.optional(),
|
|
explicit: caskDirectoriesSchema.optional(),
|
|
})
|
|
.loose();
|
|
|
|
/**
|
|
* Read the application directory Homebrew recorded when it installed a cask,
|
|
* in Homebrew's precedence: explicit option, then environment, then default.
|
|
* Reading the Caskroom avoids `brew` cask commands, which refresh Homebrew's
|
|
* API and bootsnap caches.
|
|
*/
|
|
export const installedCaskAppdir = async (
|
|
caskroom: string,
|
|
token: string,
|
|
home: string,
|
|
): Promise<string | undefined> => {
|
|
let text: string;
|
|
try {
|
|
text = await readFile(
|
|
join(caskroom, token, ".metadata", "config.json"),
|
|
"utf8",
|
|
);
|
|
} catch (cause: unknown) {
|
|
// An absent or unreadable receipt means the cask is not installed here.
|
|
void cause;
|
|
return undefined;
|
|
}
|
|
let parsed: unknown;
|
|
try {
|
|
parsed = JSON.parse(text);
|
|
} catch (cause: unknown) {
|
|
// A malformed receipt does not identify an application directory.
|
|
void cause;
|
|
return undefined;
|
|
}
|
|
const config = caskConfigSchema.safeParse(parsed);
|
|
if (!config.success) return undefined;
|
|
const appdir =
|
|
config.data.explicit?.appdir ??
|
|
config.data.env?.appdir ??
|
|
config.data.default?.appdir;
|
|
if (appdir === undefined) return undefined;
|
|
return appdir.startsWith("~/") ? join(home, appdir.slice(2)) : appdir;
|
|
};
|