├── 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 | [](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 | 
61 | 
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 |
--------------------------------------------------------------------------------