Files
reverse-engineering-anything/src/application/homebrew.ts
T
Kaoru0822andClaude Opus 5.5 a503376b2f fix(doctor): find the Homebrew Hopper cask without writing Homebrew caches
`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>
2026-10-09 03:28:25 +00:00

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;
};