Created
November 23, 2024 09:52
-
-
Save Strajk/5481a80bbc030119aece536d8a47497b to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // 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.dmg | |
| import "@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 change | |
| let 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 volumes | |
| let 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-line | |
| let 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.0 | |
| let 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]) | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment