├── dockhunt.sh ├── .gitignore ├── .npmignore ├── images ├── figma-1.png └── figma-2.png ├── rollup.config.js ├── entitlements.plist ├── index.js ├── .github └── FUNDING.yml ├── dev-tools └── iconify.bash ├── package.json ├── README.md └── utils.js /dockhunt.sh: -------------------------------------------------------------------------------- 1 | ./dockhunt 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | .idea 4 | temp_* 5 | dist 6 | dockhunt 7 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Ignores the executable 2 | dockhunt 3 | .idea 4 | dist 5 | rollup.config.js 6 | -------------------------------------------------------------------------------- /images/figma-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Basedash/dockhunt-cli/HEAD/images/figma-1.png -------------------------------------------------------------------------------- /images/figma-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Basedash/dockhunt-cli/HEAD/images/figma-2.png -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import { nodeResolve } from '@rollup/plugin-node-resolve'; 2 | import commonjs from '@rollup/plugin-commonjs'; 3 | import shebang from 'rollup-plugin-preserve-shebang'; 4 | 5 | export default { 6 | input: 'index.js', 7 | output: { 8 | format: 'cjs', 9 | dir: 'dist', 10 | entryFileNames: '[name].cjs' 11 | }, 12 | plugins: [commonjs(), nodeResolve(), shebang()] 13 | }; 14 | -------------------------------------------------------------------------------- /entitlements.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.cs.allow-jit 6 | 7 | com.apple.security.cs.allow-unsigned-executable-memory 8 | 9 | com.apple.security.cs.disable-library-validation 10 | 11 | com.apple.security.cs.allow-dyld-environment-variables 12 | 13 | com.apple.security.cs.debugger 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import child_process from 'child_process'; 4 | import {scanDockAndBringToWebApp} from "./utils.js"; 5 | 6 | // Entry point for the Dockhunt CLI 7 | 8 | // TODO: Don't allow to run and show an error, unless on macOS 9 | 10 | console.log('Scanning your dock...\n') 11 | 12 | child_process.exec('defaults export com.apple.dock -', (error, stdout, stderr) => { 13 | if (error) { 14 | console.error(`error: ${error.message}`); 15 | return; 16 | } 17 | 18 | if (stderr) { 19 | console.error(`stderr: ${stderr}`); 20 | return; 21 | } 22 | 23 | 24 | scanDockAndBringToWebApp(stdout); 25 | }) 26 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | custom: ['https://www.basedash.com'] 14 | -------------------------------------------------------------------------------- /dev-tools/iconify.bash: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Turn a 1024x1024 PNG into an ICNS icon 4 | # 5 | # https://stackoverflow.com/a/20703594/15487978 6 | 7 | input_filename='basedash-dev.png' 8 | output_basename='BasedashDev' 9 | 10 | mkdir $output_basename.iconset 11 | sips -z 16 16 $input_filename --out $output_basename.iconset/icon_16x16.png 12 | sips -z 32 32 $input_filename --out $output_basename.iconset/icon_16x16@2x.png 13 | sips -z 32 32 $input_filename --out $output_basename.iconset/icon_32x32.png 14 | sips -z 64 64 $input_filename --out $output_basename.iconset/icon_32x32@2x.png 15 | sips -z 128 128 $input_filename --out $output_basename.iconset/icon_128x128.png 16 | sips -z 256 256 $input_filename --out $output_basename.iconset/icon_128x128@2x.png 17 | sips -z 256 256 $input_filename --out $output_basename.iconset/icon_256x256.png 18 | sips -z 512 512 $input_filename --out $output_basename.iconset/icon_256x256@2x.png 19 | sips -z 512 512 $input_filename --out $output_basename.iconset/icon_512x512.png 20 | 21 | cp $input_filename $output_basename.iconset/icon_512x512@2x.png 22 | iconutil -c icns $output_basename.iconset 23 | rm -R $output_basename.iconset 24 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dockhunt", 3 | "version": "1.0.17", 4 | "description": "Share which apps you have in your macOS dock with Dockhunt.", 5 | "os": [ 6 | "darwin" 7 | ], 8 | "type": "module", 9 | "main": "index.js", 10 | "bin": { 11 | "dockhunt": "./index.js" 12 | }, 13 | "scripts": { 14 | "build": "rm -rf dist && rollup --config", 15 | "package": "npm run build && pkg --targets node16-macos-x64 --output dockhunt ./dist/index.cjs" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "git+https://github.com/Basedash/dockhunt-cli.git" 20 | }, 21 | "author": "Basedash engineering", 22 | "license": "ISC", 23 | "bugs": { 24 | "url": "https://github.com/Basedash/dockhunt-cli/issues" 25 | }, 26 | "homepage": "https://github.com/Basedash/dockhunt-cli#readme", 27 | "dependencies": { 28 | "fs-extra": "^11.1.0", 29 | "node-fetch": "^3.3.0", 30 | "open": "^8.4.0", 31 | "xml2js": "^0.4.23" 32 | }, 33 | "devDependencies": { 34 | "@rollup/plugin-commonjs": "^24.0.1", 35 | "@rollup/plugin-node-resolve": "^15.0.1", 36 | "pkg": "^5.8.0", 37 | "rollup": "^3.12.0", 38 | "rollup-plugin-preserve-shebang": "^1.0.1" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Dockhunt CLI 2 | 3 | [Website](https://www.dockhunt.com) ⋅ [Twitter](https://twitter.com/dockhuntapp) ⋅ [npm](https://www.npmjs.com/package/dockhunt) 4 | 5 | Scans which apps you have in your macOS dock and shares the result with 6 | Dockhunt. 7 | 8 | [![Dockhunt - Discover the apps everyone is docking about](https://user-images.githubusercontent.com/15393239/215352336-3a2e63e2-b474-45a9-9721-160cecb83325.png)](https://www.dockhunt.com) 9 | 10 | 11 | This repository is for the CLI tool. The resposity for the Dockhunt web 12 | application is: [https://github.com/Basedash/dockhunt](https://github.com/Basedash/dockhunt) 13 | 14 | ## Usage 15 | 16 | ### Option 1: Basic users 17 | 18 | Download and use the [Dockhunt Mac app](https://www.dockhunt.com/add-dock). 19 | 20 | Note: To create the Mac app, we first build an executable and then we've been 21 | using [Platypus](https://sveinbjorn.org/platypus) to package it into a macOS 22 | app bundle. 23 | 24 | ### Option 2: JavaScript developers 25 | 26 | Run the following command from your terminal 27 | 28 | ``` 29 | npx dockhunt 30 | ``` 31 | 32 | You can find the [dockhunt package](https://www.npmjs.com/package/dockhunt) on 33 | npm. 34 | 35 | ## What does it do? 36 | 1. Scans your macOS dock 37 | - For each app, find its name and the best-guess path to its icon file 38 | - Using `defaults export com.apple.dock -` (see `defaults help`) 39 | - Converts each icon file from `.icns` to `.png` 40 | - Checks if your dock contains any apps not yet known to Dockhunt 41 | - For any apps not yet known to Dockhunt, it uploads the app names and PNGs 42 | 2. Opens the Dockhunt website in your browser 43 | - You'll be invited to authenticate with Twitter to add your dock to the site 44 | - You can share your dock and see who else has the same apps in their dock 45 | 46 | # Incorrect app icons 47 | 48 | If you notice that the app icon that for an app is incorrect, it means the CLI 49 | tool picked the wrong icon file. We can resolve this if you open a GitHub issue 50 | with the name(s) of the app(s) that have incorrect icons and their 51 | corresponding `.icns` file(s). You can find the `.icns` file by right-clicking 52 | on the app from the Finder and selecting `Show Package Contents`. Then, 53 | navigate to `Contents/Resources/` and find the `.icns` file. If it's not in 54 | `Contents/Resources/`, then it may be somewhere else in the package contents. 55 | It seems you can't attach an `.icns` file in a GitHub issue, but if you compress 56 | it into a `.zip` first, it should be attachable. 57 | 58 | Here's how that looks like for Figma: 59 | 60 | ![Show package contents](./images/figma-1.png) 61 | ![Finding the icns file in the package contents](./images/figma-2.png) 62 | 63 | # Development 64 | 65 | - It is only necessary to build the app if you want to package it into an 66 | executable. The building of the app will package all the dependencies into a 67 | single file, which can then be packaged into an executable using `pkg`. 68 | -------------------------------------------------------------------------------- /utils.js: -------------------------------------------------------------------------------- 1 | import fetch, {FormData, fileFrom} from 'node-fetch'; 2 | import open from 'open'; 3 | import child_process from 'child_process'; 4 | 5 | import fs from 'fs-extra'; 6 | import path from 'path'; 7 | import querystring from 'querystring'; 8 | import url from 'url'; 9 | import util from 'util'; 10 | 11 | import {parseString} from 'xml2js'; 12 | 13 | function logWithInspect(object) { 14 | // https://stackoverflow.com/a/10729284/15487978 15 | console.log(util.inspect(object, {depth: null, colors: true})); 16 | } 17 | 18 | function isAppNameAllowed(appName) { 19 | // Finder doesn't seem to appear in the dock data 20 | const disallowedAppNames = [ 21 | 'Preview', 22 | 'Launchpad' 23 | ]; 24 | return !disallowedAppNames.includes(appName); 25 | } 26 | 27 | function getAppNamesToIconPaths(parsedDockData) { 28 | const parsedAppData = parsedDockData.plist.dict[0] 29 | 30 | //logWithInspect(parsedAppData); 31 | 32 | const persistentApps = parsedAppData.array[1].dict; 33 | const _persistentOthers = parsedAppData.array[2].dict; 34 | const _recentApps = parsedAppData.array[3].dict; 35 | 36 | const result = {}; 37 | 38 | for (const parsedAppData of persistentApps ?? []) { 39 | const appName = parsedAppData.dict[0].string[1]; 40 | const appDirectoryUrl = parsedAppData.dict[0].dict?.[0].string[0]; 41 | if (appDirectoryUrl && isAppNameAllowed(appName)) { 42 | const appDirectory = url.fileURLToPath(appDirectoryUrl) 43 | result[appName] = getIconPath(appDirectory); 44 | } 45 | } 46 | return result; 47 | } 48 | 49 | /** 50 | * 51 | * @param appNames 52 | * @returns {Promise>} 53 | */ 54 | async function getWhichAppsAreMissingFromDatabase(appNames) { 55 | if (!appNames.length) { 56 | return []; 57 | } 58 | const queryString = querystring.stringify({app: appNames}); 59 | const url = `https://www.dockhunt.com/api/cli/check-apps?${queryString}`; 60 | // const url = `http://localhost:3000/api/cli/check-apps?${queryString}`; 61 | 62 | const response = await fetch(url); 63 | if (!response.ok) { 64 | throw 'Bad response from Dockhunt `check-apps` endpoint'; 65 | } 66 | const payload = await response.json(); 67 | return payload.missingAppsInformation; 68 | } 69 | 70 | function getIconPath(appDirectory) { 71 | var appResourcesDirectory = path.join(appDirectory, 'Contents', 'Resources'); 72 | // AppName.app/Contents/Resources may not exist for Catalyst apps. 73 | if (!fs.pathExistsSync(appResourcesDirectory)) return null 74 | const files = fs.readdirSync(appResourcesDirectory) 75 | for (const file of files) { 76 | if (file.endsWith('.icns')) { 77 | return path.join(appResourcesDirectory, file); 78 | } 79 | } 80 | return null; 81 | } 82 | 83 | /** 84 | * 85 | * @param appName {string} 86 | * @param iconPath {string | null} 87 | * @returns {Promise} 88 | */ 89 | const addAppToDatabase = async (appName, iconPath) => { 90 | const URL = 'https://dockhunt.com/api/cli/icon-upload'; 91 | // const URL = 'http://localhost:3000/api/cli/icon-upload'; 92 | 93 | const form = new FormData(); 94 | 95 | form.append('app', appName); 96 | if (iconPath !== null) { 97 | form.append('icon', await fileFrom(iconPath)); 98 | } 99 | 100 | const response = await fetch(URL, { 101 | method: 'POST', 102 | body: form, 103 | }); 104 | 105 | await response.json() 106 | } 107 | 108 | export function icns2png(appName, icnsPath, outputDir) { 109 | return new Promise((resolve, reject) => { 110 | const outputPath = path.join(outputDir, appName + '.png'); 111 | console.log(`Converting icon to PNG (${appName})`); 112 | 113 | // https://stackoverflow.com/a/62892482/15487978 114 | // https://stackoverflow.com/a/10232330/15487978 115 | const sips = child_process.spawn('sips', 116 | ['-s', 'format', 'png', icnsPath, '--out', outputPath] 117 | ); 118 | 119 | sips.stdout.on('data', function (data) { 120 | //console.log('stdout: ' + data.toString()); 121 | }); 122 | 123 | sips.stderr.on('data', function (data) { 124 | console.error('stderr: ' + data.toString()); 125 | }); 126 | 127 | sips.on('exit', function (code) { 128 | if (!code === 0) { 129 | console.error('child process exited with code ' + code.toString()); 130 | reject(); 131 | } 132 | resolve({iconPath: outputPath, appName}); 133 | }); 134 | }); 135 | } 136 | 137 | export async function scanDockAndBringToWebApp(dockXmlPlist) { 138 | if (!dockXmlPlist.match(/ { 143 | parseString(dockXmlPlist, function (error, result) { 144 | return error ? reject(error) : resolve(result); 145 | }); 146 | }); 147 | 148 | const appNamesToIconPaths = getAppNamesToIconPaths(parsedDockData); 149 | const appNames = Object.keys(appNamesToIconPaths); 150 | 151 | if (appNames.length) { 152 | console.log('Found the following pinned apps in your dock:\n') 153 | for (const name of appNames) { 154 | console.log(`• ${name}`); 155 | } 156 | } else { 157 | console.log('Found what appears to be an empty dock.'); 158 | } 159 | 160 | // console.log('\nUploading missing dock icons to dockhunt...'); 161 | 162 | const appsMissingFromDatabase = await getWhichAppsAreMissingFromDatabase( 163 | appNames 164 | ); 165 | 166 | 167 | // Make a temporary dir for converted images 168 | let tempDir; 169 | if (appsMissingFromDatabase.length) { 170 | const tempDirname = `temp_${Date.now()}_icon_conversion`; 171 | tempDir = path.join(process.cwd(), tempDirname); 172 | fs.mkdirSync(tempDir); 173 | } 174 | 175 | /** @type {Promise<{iconPath: string | null, appName: string}>[]} */ 176 | const missingAppsToBeAddedToDatabasePromises = []; 177 | 178 | for (const app of appsMissingFromDatabase) { 179 | const iconPath = appNamesToIconPaths[app.name]; 180 | if (!iconPath) { 181 | console.warn(`\n${app.name} icon not found.`); 182 | } 183 | if (iconPath) { 184 | missingAppsToBeAddedToDatabasePromises.push(icns2png(app.name, iconPath, tempDir)); 185 | } else if (!iconPath && !app.foundInDb) { 186 | // We still want to upload apps to our database if they don't have an icon AND are not in our database 187 | missingAppsToBeAddedToDatabasePromises.push(new Promise((resolve) => resolve({ 188 | iconPath: null, 189 | appName: app.name 190 | }))); 191 | } 192 | } 193 | 194 | try { 195 | const missingAppsToBeAddedToDatabase = await Promise.all(missingAppsToBeAddedToDatabasePromises); 196 | 197 | /** @type {Promise[]} */ 198 | const appIconUploadPromises = []; 199 | for (const app of missingAppsToBeAddedToDatabase) { 200 | appIconUploadPromises.push(addAppToDatabase(app.appName, app.iconPath)); 201 | } 202 | 203 | // Wait for all uploads to complete 204 | await Promise.all(appIconUploadPromises); 205 | 206 | // Remove temporary directory 207 | if (tempDir) { 208 | fs.removeSync(tempDir) 209 | } 210 | 211 | // Output message saying that upload is complete 212 | console.log('\nDock scan complete!'); 213 | 214 | if (appNames.length) { 215 | const dockhuntUrl = `https://dockhunt.com/new-dock?${appNames.map(appName => `app=${encodeURIComponent(appName)}`).join('&')}`; 216 | // const dockhuntUrl = `http://localhost:3000/new-dock?${appNames.map(appName => `app=${encodeURIComponent(appName)}`).join('&')}`; 217 | 218 | console.log(`\nRedirecting to dockhunt: ${dockhuntUrl}`); 219 | await open(dockhuntUrl); 220 | } else { 221 | console.log('\nDockhunt does not currently support users making ' + 222 | 'Docks which contain no apps.'); 223 | } 224 | } catch (error) { 225 | console.error("Error converting icons to pngs:", error); 226 | } 227 | } 228 | --------------------------------------------------------------------------------