https://github.com/user-attachments/assets/cb559264-e247-4161-bb8e-27c1153751c0
Open install-dmg in Script Kit
// Name: Install app from dmg in Downloads/// Implementation notes:/// - dmg file name can be different from mounted volume name, e.g. UIBrowser302.dmg -> /Volumes/UI Browser 3.0.2.0/// - dmg might contain License agreement that needs to be accepted, e.g. UIBrowser302.dmg/// - dmg might contain other files than just the app, e.g. Extras folder and README.rtf, see UIBrowser302.dmgimport "@johnlindquist/kit"import fs, {statSync, unlinkSync} from "fs";import {join} from "path";import * as luxon from "luxon"import {execa} from "execa";import {execSync} from "child_process"let downloadsDir = home("Downloads") // Feel free to changelet dmgPaths = await globby("*.dmg", { cwd: downloadsDir })let dmgObjs = dmgPaths.map(path => ({fullPath: join(downloadsDir, path),baseName: path.split("/").pop()?.replace(".dmg", ""),createdAt: statSync(join(downloadsDir, path)).ctime.getTime(),sizeInMb: statSync(join(downloadsDir, path)).size / 1024 / 1024})).sort((a, b) => b.createdAt - a.createdAt)if (dmgObjs.length === 0) {setPlaceholder("No DMG files found in Downloads directory")} else {let selectedDmgPath = await arg({placeholder: "Which dmg?",choices: dmgObjs.map(dmg => ({value: dmg.fullPath,name: dmg.baseName,description: `${luxon.DateTime.fromMillis(dmg.createdAt).toFormat('yyyy-MM-dd HH:mm')} • ${dmg.sizeInMb.toFixed(2)} MB`}))})console.log(`Mounting ${selectedDmgPath}`)let volumeName = await attachDmg(selectedDmgPath)let mountPath = `/Volumes/${volumeName}`;console.log(`Mounted to ${mountPath}`)// Note: Globby did not work for me for mounted volumeslet apps = fs.readdirSync(mountPath).filter(f => f.endsWith(".app"))if (apps.length === 0) {setPlaceholder("No apps found in the mounted volume")// TODO: Find a better way to do early returns/exits} else {let confirmed = await arg({placeholder: `Found ${apps.length} apps: ${apps.join(", ")}, install?`,choices: ["yes", "no"]})if (confirmed !== "yes") {notify("Aborted")process.exit(0)}for (let app of apps) {console.log(`Copying ${app} to /Applications folder`);await execa(`cp`, ['-a', `${mountPath}/${app}`,'/Applications/']);}console.log(`Detaching ${mountPath}`)await detachDmg(mountPath)let confirmDeletion = await arg({placeholder: `Delete ${selectedDmgPath}?`,choices: ["yes", "no"]})if (confirmDeletion === "yes") {console.log(`Deleting ${selectedDmgPath}`)await trash(selectedDmgPath)}}}// Helpers// ===async function attachDmg(dmgPath: string): Promise<string> {// https://superuser.com/questions/221136/bypass-a-licence-agreement-when-mounting-a-dmg-on-the-command-linelet out = execSync(`yes | PAGER=cat hdiutil attach "${dmgPath}"`).toString()let lines = out.split("\n").reverse()// from the end, find line with volume name// /dev/disk6s2 Apple_HFS /Volumes/UI Browser 3.0.2.0let lineWithVolume = lines.find(line => line.includes("/Volumes/"))if (!lineWithVolume) {throw new Error(`Failed to find volume name in output: ${out}`)}let volumeName = lineWithVolume.split(`/Volumes/`)[1]return volumeName}async function detachDmg(mountPoint: string) {await execa('hdiutil', ['detach', mountPoint])}