// Name: Homebrew Installs (Last 90 Days) // Description: Lists Homebrew formulae and casks explicitly installed within the last 90 days, sorted by install time. // Author: ScottGuthart // GitHub: ScottGuthart import "@johnlindquist/kit" type Item = { type: "formula" | "cask" name: string version: string installedAt: number // ms epoch source: "json" | "receipt" | "fs" } const now = Date.now() const days90Ms = 90 * 24 * 60 * 60 * 1000 const cutoff = now - days90Ms const toMs = (n?: number | string): number | undefined => { if (n == null) return const num = typeof n === "string" ? Number(n) : n if (!Number.isFinite(num)) return // Heuristic: if seconds, convert to ms return num < 1e12 ? num * 1000 : num } const tryReadJson = async <T = any>(p: string): Promise<T | undefined> => { try { const txt = await readFile(p, "utf8") return JSON.parse(txt) } catch { return } } const getBrewValue = async (cmd: string) => { const { stdout } = await exec(cmd) return stdout.trim() } const brewPrefix = await getBrewValue("brew --prefix") const brewCellar = await getBrewValue("brew --cellar") const caskroomDir = path.join(brewPrefix, "Caskroom") const items: Item[] = [] // Gather explicit formula installs from JSON, fallback to INSTALL_RECEIPT.json or fs timestamps const gatherFormulae = async () => { let formulasJson: any[] = [] try { const { stdout } = await exec(`brew info --json=v2 --installed --formula`) const parsed = JSON.parse(stdout) if (Array.isArray(parsed)) formulasJson = parsed else if (Array.isArray(parsed?.formulae)) formulasJson = parsed.formulae } catch { // Fallback to empty; we'll try to at least list explicit formulas by name } // If JSON failed or missing, fall back to names let explicitNames: string[] = [] try { const { stdout } = await exec(`brew list --formula --installed-on-request`) explicitNames = stdout.split("\n").map(s => s.trim()).filter(Boolean) } catch { // ignore } // Index JSON by name for quick lookup const byName = new Map<string, any>() for (const f of formulasJson) { if (f?.name) byName.set(f.name, f) } const names = new Set<string>([ ...explicitNames, ...Array.from(byName.values()) .filter((f: any) => Array.isArray(f.installed) && f.installed.some((i: any) => i.installed_on_request)) .map((f: any) => f.name), ]) for (const name of names) { const f = byName.get(name) let installedAtMs: number | undefined let version = "" if (f && Array.isArray(f.installed) && f.installed.length) { // Prefer entries that were installed_on_request const explicitEntries = f.installed.filter((i: any) => i.installed_on_request) const candidates = explicitEntries.length ? explicitEntries : f.installed // Pick the most recent by available time or by last index let best = candidates[candidates.length - 1] version = best?.version || f?.versions?.stable || "" installedAtMs = toMs(best?.installed_time) ?? toMs(best?.time) ?? undefined if (!installedAtMs) { // Try receipt if (version) { const receiptPath = path.join(brewCellar, name, version, "INSTALL_RECEIPT.json") const receipt = await tryReadJson<any>(receiptPath) installedAtMs = toMs(receipt?.time) if (!installedAtMs) { // fallback to directory mtime try { const st = await stat(path.join(brewCellar, name, version)) installedAtMs = st.mtimeMs } catch {} } } } } else { // No JSON details; attempt via receipt + directory try { // Guess latest version dir const formulaDir = path.join(brewCellar, name) const versions = (await readdir(formulaDir)).filter(Boolean) // Find latest by mtime let latestVer = "" let latestTime = -1 for (const v of versions) { try { const p = path.join(formulaDir, v) const st = await stat(p) if (st.mtimeMs > latestTime) { latestTime = st.mtimeMs latestVer = v } } catch {} } version = latestVer if (latestVer) { const receiptPath = path.join(formulaDir, latestVer, "INSTALL_RECEIPT.json") const receipt = await tryReadJson<any>(receiptPath) installedAtMs = toMs(receipt?.time) ?? latestTime } } catch {} } if (installedAtMs && installedAtMs >= cutoff) { items.push({ type: "formula", name, version, installedAt: installedAtMs, source: f ? "json" : "receipt", }) } } } // Gather casks by directory timestamps (casks don't reliably expose 'installed_on_request') const gatherCasks = async () => { let caskNames: string[] = [] try { const { stdout } = await exec(`brew list --cask`) caskNames = stdout.split("\n").map(s => s.trim()).filter(Boolean) } catch { // no casks } for (const name of caskNames) { try { const base = path.join(caskroomDir, name) const exists = await pathExists(base) if (!exists) continue const sub = await readdir(base) if (!sub.length) continue let bestVer = "" let bestTime = -1 for (const v of sub) { const p = path.join(base, v) try { const st = await stat(p) const t = st.mtimeMs if (t > bestTime) { bestTime = t bestVer = v } } catch {} } if (bestTime >= cutoff) { items.push({ type: "cask", name, version: bestVer, installedAt: bestTime, source: "fs", }) } } catch { // ignore single cask errors } } } await div(md("### Scanning Homebrew installs from the last 90 days...\n\nThis may take a moment."), "p-4") await gatherFormulae() await gatherCasks() items.sort((a, b) => b.installedAt - a.installedAt) const header = ["type", "name", "version", "installedAt", "daysAgo"] const rows = [header.join(",")] for (const it of items) { const installedAtIso = new Date(it.installedAt).toISOString() const daysAgo = Math.floor((now - it.installedAt) / (24 * 60 * 60 * 1000)) rows.push([it.type, it.name, it.version || "", installedAtIso, String(daysAgo)].map(v => `"${v.replace(/"/g, '""')}"`).join(",")) } const output = rows.join("\n") await editor({ value: output, language: "properties", hint: `CSV: ${items.length} items. Copy or save as needed.`, footer: `Explicit formula installs are based on Homebrew receipts. Cask times approximated via Caskroom folder timestamps.`, })