@kud/soap-cli 1.0.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +125 -26
- package/dist/index.js +381 -0
- package/package.json +27 -14
- package/index.js +0 -120
- package/lib/file-regex.js +0 -22
- package/lib/index.js +0 -191
- package/lib/path-locations.js +0 -28
package/README.md
CHANGED
|
@@ -1,53 +1,152 @@
|
|
|
1
|
-
#
|
|
1
|
+
# soap 🧼
|
|
2
2
|
|
|
3
|
-
> I was so lazy to use any GUI to clean my macOS.
|
|
3
|
+
> _"I was so lazy to use any GUI to clean my macOS."_ — @kud
|
|
4
4
|
|
|
5
|
-
A
|
|
5
|
+
A macOS CLI that removes an app **and all its leftover files** — preferences, caches, containers, launch agents, and more.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Inspired by [AppCleaner](https://freemacsoft.net/appcleaner/) and [App Eraser](https://github.com/davunt/app-eraser), but open-source and terminal-native.
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
---
|
|
10
10
|
|
|
11
11
|
## Install
|
|
12
12
|
|
|
13
|
-
```
|
|
13
|
+
```sh
|
|
14
14
|
npm install -g @kud/soap-cli
|
|
15
15
|
```
|
|
16
16
|
|
|
17
|
+
Or via Homebrew:
|
|
18
|
+
|
|
19
|
+
```sh
|
|
20
|
+
brew install kud/tap/soap-cli
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
17
25
|
## Usage
|
|
18
26
|
|
|
19
|
-
|
|
27
|
+
```
|
|
28
|
+
soap <cask-name> Clean an app installed via Homebrew cask
|
|
29
|
+
soap <path-to-app> Clean a manually installed app
|
|
30
|
+
soap --help Show this help
|
|
31
|
+
```
|
|
20
32
|
|
|
21
|
-
|
|
33
|
+
**Examples:**
|
|
22
34
|
|
|
23
|
-
```
|
|
24
|
-
soap
|
|
25
|
-
|
|
35
|
+
```sh
|
|
36
|
+
soap spotify
|
|
37
|
+
soap android-studio
|
|
38
|
+
soap '/Applications/Android Studio.app'
|
|
26
39
|
```
|
|
27
40
|
|
|
28
|
-
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## What it does
|
|
29
44
|
|
|
30
|
-
|
|
45
|
+
### Cask mode — `soap spotify`
|
|
46
|
+
|
|
47
|
+
1. Resolves the `.app` name from `brew info`
|
|
48
|
+
2. Scans ~40 macOS directories for files matching the app name or bundle identifier
|
|
49
|
+
3. Fetches the cask's [zap stanza](https://docs.brew.sh/Cask-Cookbook#stanza-zap) — the maintainer-curated list of known leftover files
|
|
50
|
+
4. Presents a checkbox list of everything found (all pre-selected)
|
|
51
|
+
5. Moves selected files to **Trash** (recoverable from `~/.Trash`)
|
|
52
|
+
6. Optionally runs `brew uninstall --zap` to unregister the cask from Homebrew
|
|
53
|
+
|
|
54
|
+
### Path mode — `soap '/Applications/Spotify.app'`
|
|
55
|
+
|
|
56
|
+
Same as above, but skips the Homebrew step. Use this for apps installed manually from a DMG.
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## Example output
|
|
31
61
|
|
|
32
|
-
```shell
|
|
33
|
-
soap <cask-name>
|
|
34
|
-
# ex: soap android-studio
|
|
35
62
|
```
|
|
63
|
+
Welcome to soap 🧼, the app cleaner.
|
|
64
|
+
|
|
65
|
+
⠋ Fetching cask info…
|
|
66
|
+
⠋ Scanning for files…
|
|
67
|
+
|
|
68
|
+
ℹ Cleaning: Spotify
|
|
69
|
+
ℹ Mode: cask (spotify)
|
|
36
70
|
|
|
37
|
-
|
|
71
|
+
? Found 11 files. Select what to move to Trash:
|
|
72
|
+
◉ /Users/kud/Library/Application Support/Spotify
|
|
73
|
+
◉ /Users/kud/Library/Caches/com.spotify.client
|
|
74
|
+
◉ /Users/kud/Library/Preferences/com.spotify.client.plist
|
|
75
|
+
◉ /Users/kud/Library/Saved Application State/com.spotify.client.savedState
|
|
76
|
+
◉ /Users/kud/Library/Logs/Spotify
|
|
77
|
+
◉ ...
|
|
38
78
|
|
|
79
|
+
? Run `brew uninstall --zap spotify`? Yes
|
|
80
|
+
|
|
81
|
+
✔ Moved 11 file(s) to Trash.
|
|
82
|
+
|
|
83
|
+
◼ Running Homebrew uninstall…
|
|
84
|
+
✔ Done.
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## Locations scanned
|
|
90
|
+
|
|
91
|
+
`soap` scans these directories, plus `<vendor>/` subdirectories derived from the bundle ID:
|
|
92
|
+
|
|
93
|
+
| Location | What's typically there |
|
|
94
|
+
| ----------------------------------- | ------------------------------- |
|
|
95
|
+
| `/Applications` | The app bundle |
|
|
96
|
+
| `~/Library/Application Support` | App data |
|
|
97
|
+
| `~/Library/Caches` | Cached data |
|
|
98
|
+
| `~/Library/Containers` | Sandboxed app data |
|
|
99
|
+
| `~/Library/Preferences` | `.plist` config files |
|
|
100
|
+
| `~/Library/Logs` | App logs |
|
|
101
|
+
| `~/Library/LaunchAgents` | Per-user background services |
|
|
102
|
+
| `/Library/LaunchDaemons` | System-wide background services |
|
|
103
|
+
| `~/Library/HTTPStorages` | HTTP caches |
|
|
104
|
+
| `~/Library/Group Containers` | Shared app-group data |
|
|
105
|
+
| `~/Library/Saved Application State` | Window state (`.savedState`) |
|
|
106
|
+
| `~/Library/WebKit` | WebKit storage |
|
|
107
|
+
| `/private/var/db/receipts` | Package receipts |
|
|
108
|
+
| `/Library/Logs/DiagnosticReports` | Crash reports |
|
|
109
|
+
| `/Library/Audio/Plug-Ins/HAL` | Audio drivers |
|
|
110
|
+
| `~/Downloads` | Leftover `.dmg` files |
|
|
111
|
+
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
## Debug mode
|
|
115
|
+
|
|
116
|
+
```sh
|
|
117
|
+
SOAP_DEBUG=1 soap spotify
|
|
39
118
|
```
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
119
|
+
|
|
120
|
+
Enables verbose shell output — shows every subprocess command that runs.
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
## Safety
|
|
125
|
+
|
|
126
|
+
- Files are **moved to Trash**, not permanently deleted.
|
|
127
|
+
- You get a full checkbox list before anything is removed — deselect anything you want to keep.
|
|
128
|
+
- The Homebrew uninstall is a separate confirmation step.
|
|
129
|
+
- The force-uninstall fallback (for already-removed apps still registered in Homebrew) defaults to **No**.
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
## Contributing
|
|
134
|
+
|
|
135
|
+
Built with TypeScript + ESM. Requires Node 20+.
|
|
136
|
+
|
|
137
|
+
```sh
|
|
138
|
+
git clone https://github.com/kud/soap-cli.git
|
|
139
|
+
cd soap-cli
|
|
140
|
+
npm install
|
|
141
|
+
|
|
142
|
+
npm run dev # run from source via tsx
|
|
143
|
+
npm run build # compile to dist/
|
|
144
|
+
npm run typecheck # type-check without emitting
|
|
145
|
+
npm test # vitest unit tests
|
|
47
146
|
```
|
|
48
147
|
|
|
49
|
-
|
|
148
|
+
---
|
|
50
149
|
|
|
51
150
|
## Credits
|
|
52
151
|
|
|
53
|
-
Inspired
|
|
152
|
+
Inspired by [App Eraser](https://github.com/davunt/app-eraser) and [AppCleaner](https://freemacsoft.net/appcleaner/).
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// index.ts
|
|
4
|
+
import inquirer from "inquirer";
|
|
5
|
+
import signale from "signale";
|
|
6
|
+
import trash from "trash";
|
|
7
|
+
import chalk from "chalk";
|
|
8
|
+
import { $ as $2, spinner } from "zx";
|
|
9
|
+
|
|
10
|
+
// lib/index.ts
|
|
11
|
+
import { $ } from "zx";
|
|
12
|
+
import { execSync } from "child_process";
|
|
13
|
+
import fs from "fs/promises";
|
|
14
|
+
import { homedir as homedir2 } from "os";
|
|
15
|
+
|
|
16
|
+
// lib/path-locations.ts
|
|
17
|
+
import { homedir } from "os";
|
|
18
|
+
var pathLocations = [
|
|
19
|
+
"/Applications",
|
|
20
|
+
"/private/var/db/receipts",
|
|
21
|
+
"/Library/LaunchDaemons",
|
|
22
|
+
`${homedir()}/Downloads`,
|
|
23
|
+
`${homedir()}/Library`,
|
|
24
|
+
`${homedir()}/Library/Application Support`,
|
|
25
|
+
`${homedir()}/Library/Application Scripts`,
|
|
26
|
+
`${homedir()}/Library/Application Support/CrashReporter`,
|
|
27
|
+
`${homedir()}/Library/Containers`,
|
|
28
|
+
`${homedir()}/Library/Caches`,
|
|
29
|
+
`${homedir()}/Library/HTTPStorages`,
|
|
30
|
+
`${homedir()}/Library/Group Containers`,
|
|
31
|
+
`${homedir()}/Library/Internet Plug-Ins`,
|
|
32
|
+
`${homedir()}/Library/LaunchAgents`,
|
|
33
|
+
`${homedir()}/Library/Logs`,
|
|
34
|
+
"/Library/Logs/DiagnosticReports",
|
|
35
|
+
`${homedir()}/Library/Preferences`,
|
|
36
|
+
`${homedir()}/Library/Preferences/ByHost`,
|
|
37
|
+
`${homedir()}/Library/Saved Application State`,
|
|
38
|
+
`${homedir()}/Library/WebKit`,
|
|
39
|
+
`${homedir()}/Library/Caches/com.apple.helpd/Generated`,
|
|
40
|
+
"/Library/Audio/Plug-Ins/HAL"
|
|
41
|
+
];
|
|
42
|
+
var commonSuffix = ["install"];
|
|
43
|
+
|
|
44
|
+
// lib/file-regex.ts
|
|
45
|
+
var uuidReg = /[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}/g;
|
|
46
|
+
var dateReg = /[0-9]{4}-[0-9]{2}-[0-9]{2}-[0-9]{6}/g;
|
|
47
|
+
var diagReg = /.[a-zA-Z]+_resource.diag/;
|
|
48
|
+
var commonExtensions = [
|
|
49
|
+
".dmg",
|
|
50
|
+
".app",
|
|
51
|
+
".bom",
|
|
52
|
+
".plist",
|
|
53
|
+
".XPCHelper",
|
|
54
|
+
".beta",
|
|
55
|
+
".extensions",
|
|
56
|
+
".savedState",
|
|
57
|
+
".driver",
|
|
58
|
+
".wakeups_resource",
|
|
59
|
+
".diag"
|
|
60
|
+
];
|
|
61
|
+
var fileRegex = [
|
|
62
|
+
uuidReg,
|
|
63
|
+
dateReg,
|
|
64
|
+
diagReg,
|
|
65
|
+
...commonExtensions
|
|
66
|
+
];
|
|
67
|
+
|
|
68
|
+
// lib/index.ts
|
|
69
|
+
$.verbose = false;
|
|
70
|
+
var scoreThreshold = 0.4;
|
|
71
|
+
var compNameGlob = "";
|
|
72
|
+
var stripString = (file) => {
|
|
73
|
+
let transformedString = file;
|
|
74
|
+
fileRegex.forEach((regex1) => {
|
|
75
|
+
transformedString = transformedString.replace(regex1, "");
|
|
76
|
+
});
|
|
77
|
+
const normCompName = normalizeString(compNameGlob, "-").replace(/\u2019/g, "").replace(/\(/g, "").replace(/\)/g, "");
|
|
78
|
+
transformedString = transformedString.replace(normCompName, "");
|
|
79
|
+
return transformedString;
|
|
80
|
+
};
|
|
81
|
+
var normalizeString = (str, spacer = "") => str.toLowerCase().replace(/ /g, spacer);
|
|
82
|
+
var getFilePatternArray = async (appName, bundleId) => {
|
|
83
|
+
const nameVariations = createNameVariations(appName, bundleId);
|
|
84
|
+
const appNameNorm = normalizeString(appName);
|
|
85
|
+
const bundleIdNorm = normalizeString(bundleId);
|
|
86
|
+
let patternArray = [...nameVariations];
|
|
87
|
+
const appNameComponents = appNameNorm.split(".");
|
|
88
|
+
if (appNameComponents) patternArray.push(appNameComponents[0]);
|
|
89
|
+
const bundleIdComponents = bundleIdNorm.split(".");
|
|
90
|
+
if (bundleIdComponents.length > 2 && bundleIdComponents[bundleIdComponents.length - 1].toLowerCase() === "app") {
|
|
91
|
+
patternArray.push(
|
|
92
|
+
`${bundleIdComponents.slice(0, bundleIdComponents.length - 1).join(".")}`
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
const appWithSuffix = /* @__PURE__ */ new Set([]);
|
|
96
|
+
commonSuffix.forEach(
|
|
97
|
+
(suffix) => nameVariations.forEach(
|
|
98
|
+
(nameVariation) => appWithSuffix.add(`${nameVariation}${suffix}`)
|
|
99
|
+
)
|
|
100
|
+
);
|
|
101
|
+
patternArray = [...patternArray, ...appWithSuffix];
|
|
102
|
+
return patternArray;
|
|
103
|
+
};
|
|
104
|
+
var isPatternInFile = (patterns, fileToCheck) => {
|
|
105
|
+
return patterns.find((filePatten) => {
|
|
106
|
+
if (typeof filePatten !== "string") return false;
|
|
107
|
+
if (fileToCheck.includes(filePatten)) {
|
|
108
|
+
const strippedFile = stripString(fileToCheck);
|
|
109
|
+
let score = 0;
|
|
110
|
+
const indexOfString = strippedFile.indexOf(filePatten);
|
|
111
|
+
for (let i = 0; i < strippedFile.length; i += 1) {
|
|
112
|
+
if (i === indexOfString) {
|
|
113
|
+
i += indexOfString + filePatten.length;
|
|
114
|
+
score += filePatten.length;
|
|
115
|
+
}
|
|
116
|
+
if (strippedFile[parseInt(i.toString(), 10)] === ".") score += 0.5;
|
|
117
|
+
if (strippedFile[parseInt(i.toString(), 10)] === "_") score += 0.5;
|
|
118
|
+
}
|
|
119
|
+
if (score / strippedFile.length > scoreThreshold) {
|
|
120
|
+
return true;
|
|
121
|
+
}
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
return false;
|
|
125
|
+
});
|
|
126
|
+
};
|
|
127
|
+
var createNameVariations = (appName, bundleId) => {
|
|
128
|
+
const appNameNorm = appName.toLowerCase().replace(/ /g, "");
|
|
129
|
+
const appNameWithoutDot = appNameNorm.replace(/\./g, "");
|
|
130
|
+
const appNameUnderscore = appName.toLowerCase().replace(/ /g, "_");
|
|
131
|
+
const appNameDash = appName.toLowerCase().replace(/ /g, "-");
|
|
132
|
+
const appNameDot = appName.toLowerCase().replace(/ /g, ".");
|
|
133
|
+
const bundleIdNorm = bundleId.toLowerCase().replace(/ /g, "");
|
|
134
|
+
return [
|
|
135
|
+
appNameNorm,
|
|
136
|
+
appNameWithoutDot,
|
|
137
|
+
appNameUnderscore,
|
|
138
|
+
appNameDash,
|
|
139
|
+
appNameDot,
|
|
140
|
+
bundleIdNorm
|
|
141
|
+
];
|
|
142
|
+
};
|
|
143
|
+
var appNameFromPath = (appPath) => {
|
|
144
|
+
const pathArr = appPath.split("/");
|
|
145
|
+
const appNameWithExt = pathArr[pathArr.length - 1];
|
|
146
|
+
return appNameWithExt.replace(".app", "");
|
|
147
|
+
};
|
|
148
|
+
var fetchCaskData = async (caskName) => {
|
|
149
|
+
const { stdout: info } = await $`brew info ${caskName} --json=v2`;
|
|
150
|
+
const caskData = JSON.parse(info).casks?.[0];
|
|
151
|
+
if (!caskData) throw new Error(`Cask "${caskName}" not found.`);
|
|
152
|
+
return caskData;
|
|
153
|
+
};
|
|
154
|
+
var resolveZapPaths = async (caskData) => {
|
|
155
|
+
const zapPaths = caskData.artifacts.filter((a) => a.zap).flatMap((a) => a.zap).filter((entry) => entry?.trash).flatMap((entry) => entry?.trash ?? []).map((p) => p.replace(/^~/, homedir2()));
|
|
156
|
+
const resolved = await Promise.all(
|
|
157
|
+
zapPaths.map(async (p) => {
|
|
158
|
+
if (p.includes("*")) {
|
|
159
|
+
const matches = [];
|
|
160
|
+
for await (const f of fs.glob(p)) matches.push(f);
|
|
161
|
+
return matches;
|
|
162
|
+
}
|
|
163
|
+
try {
|
|
164
|
+
await fs.access(p);
|
|
165
|
+
return [p];
|
|
166
|
+
} catch {
|
|
167
|
+
return [];
|
|
168
|
+
}
|
|
169
|
+
})
|
|
170
|
+
);
|
|
171
|
+
return resolved.flat();
|
|
172
|
+
};
|
|
173
|
+
var getCaskInfo = async (caskName) => {
|
|
174
|
+
try {
|
|
175
|
+
const caskData = await fetchCaskData(caskName);
|
|
176
|
+
let appName = null;
|
|
177
|
+
for (const artifact of caskData.artifacts) {
|
|
178
|
+
if (artifact.app) {
|
|
179
|
+
appName = artifact.app[0];
|
|
180
|
+
break;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
const zapFiles = await resolveZapPaths(caskData);
|
|
184
|
+
return { appName, zapFiles };
|
|
185
|
+
} catch (error) {
|
|
186
|
+
throw new Error(
|
|
187
|
+
`Failed to get cask info for "${caskName}": ${error instanceof Error ? error.message : String(error)}`
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
var getComputerName = () => {
|
|
192
|
+
return execSync("scutil --get ComputerName").toString().trimEnd();
|
|
193
|
+
};
|
|
194
|
+
var getBundleIdentifier = async (appName) => {
|
|
195
|
+
try {
|
|
196
|
+
const bundleId = execSync(
|
|
197
|
+
`osascript -e 'id of app "${appName}"'`
|
|
198
|
+
).toString();
|
|
199
|
+
return bundleId.trimEnd();
|
|
200
|
+
} catch {
|
|
201
|
+
throw new Error(
|
|
202
|
+
`Could not find bundle identifier for "${appName}". Is the app installed?`
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
var findAppFiles = async (appName, bundleId) => {
|
|
207
|
+
try {
|
|
208
|
+
compNameGlob = getComputerName();
|
|
209
|
+
const bundleIdComponents = bundleId.split(".");
|
|
210
|
+
const companyDirs = pathLocations.map(
|
|
211
|
+
(pathLocation) => `${pathLocation}/${bundleIdComponents[1]}`
|
|
212
|
+
);
|
|
213
|
+
const pathsToSearch = [...pathLocations, ...companyDirs];
|
|
214
|
+
const directoryFilesPromiseArr = pathsToSearch.map(
|
|
215
|
+
(pathLocation) => fs.readdir(pathLocation)
|
|
216
|
+
);
|
|
217
|
+
const directoryFiles = await Promise.allSettled(directoryFilesPromiseArr);
|
|
218
|
+
const patternArray = await getFilePatternArray(appName, bundleId);
|
|
219
|
+
const filesToRemove = /* @__PURE__ */ new Set([]);
|
|
220
|
+
directoryFiles.forEach((dir, index) => {
|
|
221
|
+
if (dir.status === "fulfilled") {
|
|
222
|
+
dir.value.forEach((dirFile) => {
|
|
223
|
+
const dirFileNorm = dirFile.toLowerCase().replace(/ /g, "");
|
|
224
|
+
if (isPatternInFile(patternArray, dirFileNorm)) {
|
|
225
|
+
filesToRemove.add(
|
|
226
|
+
`${pathsToSearch[parseInt(index.toString(), 10)]}/${dirFile}`
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
return [...filesToRemove];
|
|
233
|
+
} catch (err) {
|
|
234
|
+
console.error(err);
|
|
235
|
+
throw err;
|
|
236
|
+
}
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
// index.ts
|
|
240
|
+
$2.verbose = process.env.SOAP_DEBUG === "1";
|
|
241
|
+
var [param] = process.argv.slice(2);
|
|
242
|
+
var isFlag = param?.startsWith("-");
|
|
243
|
+
if (!param || param === "--help" || param === "-h" || param === "--version" || param === "-v" || isFlag && param !== "--help" && param !== "-h") {
|
|
244
|
+
if (param === "--version" || param === "-v") {
|
|
245
|
+
const { createRequire } = await import("module");
|
|
246
|
+
const require2 = createRequire(import.meta.url);
|
|
247
|
+
const { version } = require2("./package.json");
|
|
248
|
+
console.log(`soap v${version}`);
|
|
249
|
+
process.exit(0);
|
|
250
|
+
}
|
|
251
|
+
if (param === "--help" || param === "-h") {
|
|
252
|
+
console.log(`
|
|
253
|
+
${chalk.bold("soap")} \u{1F9FC} ${chalk.italic("the app cleaner")}
|
|
254
|
+
|
|
255
|
+
${chalk.bold("Usage:")}
|
|
256
|
+
soap ${chalk.cyan("<cask-name>")} | ${chalk.cyan("<path-to-app>")}
|
|
257
|
+
|
|
258
|
+
${chalk.bold("Examples:")}
|
|
259
|
+
soap ${chalk.green("spotify")} Uninstall Spotify (cask) + all its leftover files
|
|
260
|
+
soap ${chalk.green("android-studio")} Uninstall Android Studio (cask)
|
|
261
|
+
soap ${chalk.green("/Applications/Slack.app")}
|
|
262
|
+
Uninstall Slack by path (no brew step)
|
|
263
|
+
|
|
264
|
+
${chalk.bold("What it removes:")}
|
|
265
|
+
${chalk.dim("\xB7")} The .app bundle ${chalk.dim("(via brew uninstall --zap or manual selection)")}
|
|
266
|
+
${chalk.dim("\xB7")} Preferences ${chalk.dim("~/Library/Preferences/com.<vendor>.<app>.plist")}
|
|
267
|
+
${chalk.dim("\xB7")} Caches ${chalk.dim("~/Library/Caches/com.<vendor>.<app>")}
|
|
268
|
+
${chalk.dim("\xB7")} App support ${chalk.dim("~/Library/Application Support/<App>")}
|
|
269
|
+
${chalk.dim("\xB7")} Containers ${chalk.dim("~/Library/Containers/com.<vendor>.<app>")}
|
|
270
|
+
${chalk.dim("\xB7")} Launch agents, logs, crash reports, DMG files, and more
|
|
271
|
+
|
|
272
|
+
${chalk.bold("Environment:")}
|
|
273
|
+
${chalk.yellow("SOAP_DEBUG=1")} Enable verbose shell output
|
|
274
|
+
`);
|
|
275
|
+
process.exit(0);
|
|
276
|
+
}
|
|
277
|
+
if (isFlag) {
|
|
278
|
+
console.error(
|
|
279
|
+
chalk.red(`Unknown option: ${param}. Run \`soap --help\` for usage.`)
|
|
280
|
+
);
|
|
281
|
+
} else {
|
|
282
|
+
console.error(
|
|
283
|
+
chalk.red("No parameter specified. Run `soap --help` for usage.")
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
process.exit(1);
|
|
287
|
+
}
|
|
288
|
+
var isCask = !param.includes(".app");
|
|
289
|
+
console.log(
|
|
290
|
+
`
|
|
291
|
+
Welcome to ${chalk.bold("soap")} \u{1F9FC}, ${chalk.italic("the app cleaner")}.
|
|
292
|
+
`
|
|
293
|
+
);
|
|
294
|
+
try {
|
|
295
|
+
const { appName, zapFiles } = isCask ? await spinner(chalk.dim("Fetching cask info\u2026"), () => getCaskInfo(param)) : { appName: appNameFromPath(param), zapFiles: [] };
|
|
296
|
+
if (!appName) {
|
|
297
|
+
signale.error(`Could not determine app name for "${param}".`);
|
|
298
|
+
process.exit(1);
|
|
299
|
+
}
|
|
300
|
+
const bundleId = await getBundleIdentifier(appName);
|
|
301
|
+
const scannedFiles = await spinner(
|
|
302
|
+
chalk.dim("Scanning for files\u2026"),
|
|
303
|
+
() => findAppFiles(appName, bundleId)
|
|
304
|
+
);
|
|
305
|
+
const appFiles = [
|
|
306
|
+
.../* @__PURE__ */ new Set([
|
|
307
|
+
...isCask ? scannedFiles.slice(1) : scannedFiles,
|
|
308
|
+
...zapFiles
|
|
309
|
+
])
|
|
310
|
+
];
|
|
311
|
+
const isAppFilesEmpty = appFiles.length === 0;
|
|
312
|
+
signale.info(`Cleaning: ${chalk.bold(appName)}`);
|
|
313
|
+
signale.info(
|
|
314
|
+
isCask ? `Mode: ${chalk.bold("cask")} (${param})` : `Mode: ${chalk.bold("path")} \u2014 no Homebrew uninstall will run`
|
|
315
|
+
);
|
|
316
|
+
console.log("");
|
|
317
|
+
const { deletedFilesWish, deletedCaskWish } = await inquirer.prompt([
|
|
318
|
+
{
|
|
319
|
+
type: "checkbox",
|
|
320
|
+
name: "deletedFilesWish",
|
|
321
|
+
when: !isAppFilesEmpty,
|
|
322
|
+
message: `Found ${chalk.bold(appFiles.length)} files. Select what to move to Trash:`,
|
|
323
|
+
choices: appFiles.map((appFile) => ({
|
|
324
|
+
name: appFile,
|
|
325
|
+
value: appFile,
|
|
326
|
+
checked: true
|
|
327
|
+
}))
|
|
328
|
+
},
|
|
329
|
+
{
|
|
330
|
+
type: "confirm",
|
|
331
|
+
when: isCask,
|
|
332
|
+
name: "deletedCaskWish",
|
|
333
|
+
message: `Run ${chalk.bold(`brew uninstall --zap ${param}`)}?`,
|
|
334
|
+
default: true
|
|
335
|
+
}
|
|
336
|
+
]);
|
|
337
|
+
if (!isAppFilesEmpty && deletedFilesWish && deletedFilesWish.length > 0) {
|
|
338
|
+
await trash(deletedFilesWish);
|
|
339
|
+
console.log("");
|
|
340
|
+
signale.success(
|
|
341
|
+
`Moved ${chalk.bold(deletedFilesWish.length)} file(s) to Trash:`
|
|
342
|
+
);
|
|
343
|
+
deletedFilesWish.forEach(
|
|
344
|
+
(p) => console.log(` ${chalk.dim("\xB7")} ${chalk.dim(p)}`)
|
|
345
|
+
);
|
|
346
|
+
} else if (!isAppFilesEmpty) {
|
|
347
|
+
signale.info("No files selected \u2014 nothing moved to Trash.");
|
|
348
|
+
}
|
|
349
|
+
if (isCask && deletedCaskWish) {
|
|
350
|
+
console.log("");
|
|
351
|
+
signale.pending("Running Homebrew uninstall\u2026");
|
|
352
|
+
await $2`brew uninstall --zap ${param}`;
|
|
353
|
+
}
|
|
354
|
+
console.log("");
|
|
355
|
+
signale.success("Done.");
|
|
356
|
+
} catch (error) {
|
|
357
|
+
console.log("");
|
|
358
|
+
signale.error(
|
|
359
|
+
error instanceof Error ? error.message : "An unexpected error occurred."
|
|
360
|
+
);
|
|
361
|
+
if (process.env.SOAP_DEBUG === "1") {
|
|
362
|
+
console.error(error);
|
|
363
|
+
}
|
|
364
|
+
if (isCask) {
|
|
365
|
+
const { forceUninstall } = await inquirer.prompt([
|
|
366
|
+
{
|
|
367
|
+
type: "confirm",
|
|
368
|
+
name: "forceUninstall",
|
|
369
|
+
message: `The app may already be removed but the cask is still registered.
|
|
370
|
+
Force-run ${chalk.bold(`brew uninstall --zap --force ${param}`)}?`,
|
|
371
|
+
default: false
|
|
372
|
+
}
|
|
373
|
+
]);
|
|
374
|
+
if (forceUninstall) {
|
|
375
|
+
console.log("");
|
|
376
|
+
signale.pending("Force-uninstalling cask\u2026");
|
|
377
|
+
await $2`brew uninstall --zap --force ${param}`;
|
|
378
|
+
signale.success("Force uninstall complete.");
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
package/package.json
CHANGED
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kud/soap-cli",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.0",
|
|
4
4
|
"description": "An app cleaner cli for macOS",
|
|
5
|
-
"main": "index.js",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
7
|
-
"soap": "index.js"
|
|
8
|
-
},
|
|
9
|
-
"directories": {
|
|
10
|
-
"lib": "lib"
|
|
7
|
+
"soap": "dist/index.js"
|
|
11
8
|
},
|
|
12
9
|
"scripts": {
|
|
13
|
-
"dev": "
|
|
14
|
-
"
|
|
10
|
+
"dev": "tsx index.ts",
|
|
11
|
+
"build": "tsup",
|
|
12
|
+
"postbuild": "chmod +x dist/index.js",
|
|
13
|
+
"typecheck": "tsc --noEmit",
|
|
14
|
+
"clean": "rm -rf dist",
|
|
15
|
+
"test": "vitest run",
|
|
16
|
+
"prepublishOnly": "npm run build"
|
|
15
17
|
},
|
|
16
18
|
"repository": {
|
|
17
19
|
"type": "git",
|
|
@@ -25,13 +27,24 @@
|
|
|
25
27
|
},
|
|
26
28
|
"homepage": "https://github.com/kud/soap-cli#readme",
|
|
27
29
|
"type": "module",
|
|
30
|
+
"files": [
|
|
31
|
+
"dist"
|
|
32
|
+
],
|
|
28
33
|
"dependencies": {
|
|
29
|
-
"@kud/soap-cli": "1.0.0
|
|
30
|
-
"chalk": "5.
|
|
31
|
-
"del": "
|
|
32
|
-
"inquirer": "
|
|
34
|
+
"@kud/soap-cli": "1.0.0",
|
|
35
|
+
"chalk": "5.6.2",
|
|
36
|
+
"del": "8.0.1",
|
|
37
|
+
"inquirer": "13.3.2",
|
|
33
38
|
"signale": "1.4.0",
|
|
34
|
-
"trash": "
|
|
35
|
-
"zx": "8.
|
|
39
|
+
"trash": "10.1.1",
|
|
40
|
+
"zx": "8.8.5"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"@types/node": "25.5.0",
|
|
44
|
+
"@types/signale": "1.4.7",
|
|
45
|
+
"tsup": "8.5.1",
|
|
46
|
+
"tsx": "4.21.0",
|
|
47
|
+
"typescript": "6.0.2",
|
|
48
|
+
"vitest": "4.1.2"
|
|
36
49
|
}
|
|
37
50
|
}
|
package/index.js
DELETED
|
@@ -1,120 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import inquirer from "inquirer"
|
|
3
|
-
import signale from "signale"
|
|
4
|
-
import trash from "trash"
|
|
5
|
-
import chalk from "chalk"
|
|
6
|
-
import { $ } from "zx/core"
|
|
7
|
-
|
|
8
|
-
import {
|
|
9
|
-
appNameFromPath,
|
|
10
|
-
appNameFromCaskName,
|
|
11
|
-
getBundleIdentifier,
|
|
12
|
-
findAppFiles,
|
|
13
|
-
} from "./lib/index.js"
|
|
14
|
-
|
|
15
|
-
$.verbose = false // true for debugging
|
|
16
|
-
|
|
17
|
-
const [param] = process.argv.slice(2)
|
|
18
|
-
|
|
19
|
-
if (!param) {
|
|
20
|
-
console.error(`No parameter specified.`)
|
|
21
|
-
|
|
22
|
-
process.exit(0)
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
const isCask = !param.includes(".app")
|
|
26
|
-
|
|
27
|
-
console.log(
|
|
28
|
-
`Welcome to ${chalk.bold("soap")} 🧼, ${chalk.italic("the app cleaner")}.\n`,
|
|
29
|
-
)
|
|
30
|
-
|
|
31
|
-
try {
|
|
32
|
-
const appName = isCask
|
|
33
|
-
? await appNameFromCaskName(param)
|
|
34
|
-
: appNameFromPath(param)
|
|
35
|
-
|
|
36
|
-
const bundleId = await getBundleIdentifier(appName)
|
|
37
|
-
const _appFiles = await findAppFiles(appName, bundleId)
|
|
38
|
-
const appFiles = isCask ? _appFiles.slice(1) : _appFiles
|
|
39
|
-
const isAppFilesEmpty = appFiles.length === 0
|
|
40
|
-
|
|
41
|
-
signale.info(
|
|
42
|
-
`You want me to clean this application: ${chalk.bold(appName)} 📦.`,
|
|
43
|
-
)
|
|
44
|
-
|
|
45
|
-
signale.info(
|
|
46
|
-
isCask
|
|
47
|
-
? `I also assume ${chalk.bold("you gave me a cask name")}.`
|
|
48
|
-
: `I also assume you gave me an application path. ${chalk.bold(
|
|
49
|
-
"No homebrew cask will be deleted then",
|
|
50
|
-
)}.`,
|
|
51
|
-
)
|
|
52
|
-
|
|
53
|
-
console.log("")
|
|
54
|
-
|
|
55
|
-
const { deletedFilesWish } = await inquirer.prompt([
|
|
56
|
-
{
|
|
57
|
-
type: "checkbox",
|
|
58
|
-
name: "deletedFilesWish",
|
|
59
|
-
when: !isAppFilesEmpty,
|
|
60
|
-
message:
|
|
61
|
-
"This is what I've found about it. Please select the files you want to delete.",
|
|
62
|
-
choices: appFiles.map((appFile) => ({
|
|
63
|
-
name: appFile,
|
|
64
|
-
value: appFile,
|
|
65
|
-
checked: true,
|
|
66
|
-
})),
|
|
67
|
-
},
|
|
68
|
-
{
|
|
69
|
-
type: "confirm",
|
|
70
|
-
when: isCask,
|
|
71
|
-
name: "deletedCaskWish",
|
|
72
|
-
message: `Do you want to uninstall "${param}" via homebrew?`,
|
|
73
|
-
},
|
|
74
|
-
])
|
|
75
|
-
|
|
76
|
-
if (!isAppFilesEmpty) {
|
|
77
|
-
await trash(deletedFilesWish, { force: true })
|
|
78
|
-
|
|
79
|
-
console.log("")
|
|
80
|
-
|
|
81
|
-
console.log("Files deleted:")
|
|
82
|
-
deletedFilesWish.forEach((deletedPath) => console.log(`· ${deletedPath}`))
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
if (isCask) {
|
|
86
|
-
console.log("")
|
|
87
|
-
|
|
88
|
-
signale.pending(`Starting cask uninstallation.`)
|
|
89
|
-
|
|
90
|
-
await $`brew uninstall ${param}`
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
console.log("")
|
|
94
|
-
|
|
95
|
-
signale.success("Operation successful.")
|
|
96
|
-
} catch (error) {
|
|
97
|
-
signale.error(`Something wrong appeared. Check the log below.\n`)
|
|
98
|
-
console.error(error)
|
|
99
|
-
console.error("")
|
|
100
|
-
|
|
101
|
-
if (isCask) {
|
|
102
|
-
inquirer
|
|
103
|
-
.prompt([
|
|
104
|
-
{
|
|
105
|
-
type: "confirm",
|
|
106
|
-
name: "forceUninstall",
|
|
107
|
-
message:
|
|
108
|
-
"An error occurred. It could happen when the app file no longer exists, but the cask is still present in Homebrew.\nWould you like to forcefully uninstall the cask by running `brew uninstall`?",
|
|
109
|
-
},
|
|
110
|
-
])
|
|
111
|
-
.then(async (answers) => {
|
|
112
|
-
if (answers.forceUninstall) {
|
|
113
|
-
console.error("")
|
|
114
|
-
signale.pending(`Forcefully uninstalling cask.`)
|
|
115
|
-
await $`brew uninstall --force ${param}`
|
|
116
|
-
signale.success("Force uninstallation successful.")
|
|
117
|
-
}
|
|
118
|
-
})
|
|
119
|
-
}
|
|
120
|
-
}
|
package/lib/file-regex.js
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
const uuidReg =
|
|
2
|
-
/[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}/g
|
|
3
|
-
|
|
4
|
-
const dateReg = /[0-9]{4}-[0-9]{2}-[0-9]{2}-[0-9]{6}/g
|
|
5
|
-
|
|
6
|
-
const diagReg = /.[a-zA-Z]+_resource.diag/
|
|
7
|
-
|
|
8
|
-
const commonExtensions = [
|
|
9
|
-
".dmg",
|
|
10
|
-
".app",
|
|
11
|
-
".bom",
|
|
12
|
-
".plist",
|
|
13
|
-
".XPCHelper",
|
|
14
|
-
".beta",
|
|
15
|
-
".extensions",
|
|
16
|
-
".savedState",
|
|
17
|
-
".driver",
|
|
18
|
-
".wakeups_resource",
|
|
19
|
-
".diag",
|
|
20
|
-
]
|
|
21
|
-
|
|
22
|
-
export const fileRegex = [uuidReg, dateReg, diagReg, ...commonExtensions]
|
package/lib/index.js
DELETED
|
@@ -1,191 +0,0 @@
|
|
|
1
|
-
import { $ } from "zx"
|
|
2
|
-
$.verbose = false
|
|
3
|
-
|
|
4
|
-
import { execSync } from "child_process"
|
|
5
|
-
import fs from "fs/promises"
|
|
6
|
-
import { pathLocations, commonSuffix } from "./path-locations.js"
|
|
7
|
-
import { fileRegex } from "./file-regex.js"
|
|
8
|
-
|
|
9
|
-
const scoreThreshold = 0.4
|
|
10
|
-
|
|
11
|
-
let compNameGlob
|
|
12
|
-
|
|
13
|
-
function stripString(file) {
|
|
14
|
-
let transformedString = file
|
|
15
|
-
fileRegex.forEach((regex1) => {
|
|
16
|
-
transformedString = transformedString.replace(regex1, "")
|
|
17
|
-
})
|
|
18
|
-
|
|
19
|
-
const normCompName = normalizeString(compNameGlob, "-")
|
|
20
|
-
.replace(/\u2019/g, "")
|
|
21
|
-
.replace(/\(/g, "")
|
|
22
|
-
.replace(/\)/g, "")
|
|
23
|
-
|
|
24
|
-
transformedString = transformedString.replace(normCompName, "")
|
|
25
|
-
|
|
26
|
-
return transformedString
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
const normalizeString = (str, spacer = "") =>
|
|
30
|
-
str.toLowerCase().replace(/ /g, spacer)
|
|
31
|
-
|
|
32
|
-
async function getFilePatternArray(appName, bundleId) {
|
|
33
|
-
const nameVariations = createNameVariations(appName, bundleId)
|
|
34
|
-
const appNameNorm = normalizeString(appName)
|
|
35
|
-
const bundleIdNorm = normalizeString(bundleId)
|
|
36
|
-
|
|
37
|
-
let patternArray = [...nameVariations]
|
|
38
|
-
|
|
39
|
-
const appNameComponents = appNameNorm.split(".")
|
|
40
|
-
if (appNameComponents) patternArray.push(appNameComponents[0])
|
|
41
|
-
|
|
42
|
-
const bundleIdComponents = bundleIdNorm.split(".")
|
|
43
|
-
if (
|
|
44
|
-
bundleIdComponents.length > 2 &&
|
|
45
|
-
bundleIdComponents[bundleIdComponents.length - 1].toLowerCase() === "app"
|
|
46
|
-
) {
|
|
47
|
-
patternArray.push(
|
|
48
|
-
`${bundleIdComponents.slice(0, bundleIdComponents.length - 1).join(".")}`,
|
|
49
|
-
)
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
const appWithSuffix = new Set([])
|
|
53
|
-
commonSuffix.forEach((suffix) =>
|
|
54
|
-
nameVariations.forEach((nameVariation) =>
|
|
55
|
-
appWithSuffix.add(`${nameVariation}${suffix}`),
|
|
56
|
-
),
|
|
57
|
-
)
|
|
58
|
-
|
|
59
|
-
patternArray = [...patternArray, [...appWithSuffix]]
|
|
60
|
-
|
|
61
|
-
return patternArray
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function isPatternInFile(patterns, fileToCheck) {
|
|
65
|
-
return patterns.find((filePatten) => {
|
|
66
|
-
if (fileToCheck.includes(filePatten)) {
|
|
67
|
-
const strippedFile = stripString(fileToCheck)
|
|
68
|
-
|
|
69
|
-
let score = 0
|
|
70
|
-
const indexOfString = strippedFile.indexOf(filePatten)
|
|
71
|
-
for (let i = 0; i < strippedFile.length; i += 1) {
|
|
72
|
-
if (i === indexOfString) {
|
|
73
|
-
i += indexOfString + filePatten.length
|
|
74
|
-
score += filePatten.length
|
|
75
|
-
}
|
|
76
|
-
if (strippedFile[parseInt(i, 10)] === ".") score += 0.5
|
|
77
|
-
if (strippedFile[parseInt(i, 10)] === "_") score += 0.5
|
|
78
|
-
}
|
|
79
|
-
if (score / strippedFile.length > scoreThreshold) {
|
|
80
|
-
return true
|
|
81
|
-
}
|
|
82
|
-
return false
|
|
83
|
-
}
|
|
84
|
-
return false
|
|
85
|
-
})
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
function createNameVariations(appName, bundleId) {
|
|
89
|
-
const appNameNorm = appName.toLowerCase().replace(" ", "")
|
|
90
|
-
const appNameWithoutDot = appNameNorm.toLowerCase().replace(".", "")
|
|
91
|
-
const appNameUnderscore = appName.toLowerCase().replace(" ", "_")
|
|
92
|
-
const appNameDash = appName.toLowerCase().replace(" ", "-")
|
|
93
|
-
const appNameDot = appName.toLowerCase().replace(" ", ".")
|
|
94
|
-
|
|
95
|
-
const bundleIdNorm = bundleId.toLowerCase().replace(" ", "")
|
|
96
|
-
|
|
97
|
-
return [
|
|
98
|
-
appNameNorm,
|
|
99
|
-
appNameWithoutDot,
|
|
100
|
-
appNameUnderscore,
|
|
101
|
-
appNameDash,
|
|
102
|
-
appNameDot,
|
|
103
|
-
bundleIdNorm,
|
|
104
|
-
]
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
function appNameFromPath(appPath) {
|
|
108
|
-
const pathArr = appPath.split("/")
|
|
109
|
-
const appNameWithExt = pathArr[pathArr.length - 1]
|
|
110
|
-
// remove .app extension
|
|
111
|
-
return appNameWithExt.replace(".app", "")
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
export const appNameFromCaskName = async (caskName) => {
|
|
115
|
-
try {
|
|
116
|
-
const { stdout: info } = await $`brew info ${caskName} --json=v2`
|
|
117
|
-
const caskData = JSON.parse(info).casks?.[0]
|
|
118
|
-
|
|
119
|
-
if (!caskData) {
|
|
120
|
-
throw new Error("Cask data not available.")
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
for (const artifact of caskData.artifacts) {
|
|
124
|
-
if (artifact.app) {
|
|
125
|
-
return artifact.app[0]
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
return null
|
|
130
|
-
} catch (error) {
|
|
131
|
-
console.error(`Error fetching app name for cask ${caskName}:`, error)
|
|
132
|
-
return null
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
async function getComputerName() {
|
|
137
|
-
const compName = await execSync("scutil --get ComputerName").toString()
|
|
138
|
-
|
|
139
|
-
// remove empty space at end of string
|
|
140
|
-
return compName.substring(0, compName.length - 1)
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
async function getBundleIdentifier(appName) {
|
|
144
|
-
const bundleId = await execSync(
|
|
145
|
-
`osascript -e 'id of app "${appName}"'`,
|
|
146
|
-
).toString()
|
|
147
|
-
|
|
148
|
-
// remove empty space at end of string
|
|
149
|
-
return bundleId.substring(0, bundleId.length - 1)
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
async function findAppFiles(appName, bundleId) {
|
|
153
|
-
try {
|
|
154
|
-
compNameGlob = await getComputerName()
|
|
155
|
-
const bundleIdComponents = bundleId.split(".")
|
|
156
|
-
|
|
157
|
-
const companyDirs = pathLocations.map(
|
|
158
|
-
(pathLocation) => `${pathLocation}/${bundleIdComponents[1]}`,
|
|
159
|
-
)
|
|
160
|
-
const pathsToSearch = [...pathLocations, ...companyDirs]
|
|
161
|
-
const directoryFilesPromiseArr = pathsToSearch.map((pathLocation) =>
|
|
162
|
-
fs.readdir(pathLocation),
|
|
163
|
-
)
|
|
164
|
-
const directoryFiles = await Promise.allSettled(directoryFilesPromiseArr)
|
|
165
|
-
|
|
166
|
-
const patternArray = await getFilePatternArray(appName, bundleId)
|
|
167
|
-
|
|
168
|
-
const filesToRemove = new Set([])
|
|
169
|
-
|
|
170
|
-
directoryFiles.forEach((dir, index) => {
|
|
171
|
-
if (dir.status === "fulfilled") {
|
|
172
|
-
dir.value.forEach((dirFile) => {
|
|
173
|
-
const dirFileNorm = dirFile.toLowerCase().replace(" ", "")
|
|
174
|
-
if (isPatternInFile(patternArray, dirFileNorm)) {
|
|
175
|
-
filesToRemove.add(
|
|
176
|
-
`${pathsToSearch[parseInt(index, 10)]}/${dirFile}`,
|
|
177
|
-
)
|
|
178
|
-
}
|
|
179
|
-
})
|
|
180
|
-
}
|
|
181
|
-
})
|
|
182
|
-
|
|
183
|
-
// convert set to array
|
|
184
|
-
return [...filesToRemove]
|
|
185
|
-
} catch (err) {
|
|
186
|
-
console.error(err)
|
|
187
|
-
throw err
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
export { appNameFromPath, getBundleIdentifier, findAppFiles }
|
package/lib/path-locations.js
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import { homedir } from "os"
|
|
2
|
-
|
|
3
|
-
export const pathLocations = [
|
|
4
|
-
"/Applications",
|
|
5
|
-
"/private/var/db/receipts",
|
|
6
|
-
"/Library/LaunchDaemons",
|
|
7
|
-
`${homedir}/Downloads`,
|
|
8
|
-
`${homedir}/Library`,
|
|
9
|
-
`${homedir}/Library/Application Support`,
|
|
10
|
-
`${homedir}/Library/Application Scripts`,
|
|
11
|
-
`${homedir}/Library/Application Support/CrashReporter`,
|
|
12
|
-
`${homedir}/Library/Containers`,
|
|
13
|
-
`${homedir}/Library/Caches`,
|
|
14
|
-
`${homedir}/Library/HTTPStorages`,
|
|
15
|
-
`${homedir}/Library/Group Containers`,
|
|
16
|
-
`${homedir}/Library/Internet Plug-Ins`,
|
|
17
|
-
`${homedir}/Library/LaunchAgents`,
|
|
18
|
-
`${homedir}/Library/Logs`,
|
|
19
|
-
"/Library/Logs/DiagnosticReports",
|
|
20
|
-
`${homedir}/Library/Preferences`,
|
|
21
|
-
`${homedir}/Library/Preferences/ByHost`,
|
|
22
|
-
`${homedir}/Library/Saved Application State`,
|
|
23
|
-
`${homedir}/Library/WebKit`,
|
|
24
|
-
`${homedir}/Library/Caches/com.apple.helpd/Generated`,
|
|
25
|
-
"/Library/Audio/Plug-Ins/HAL",
|
|
26
|
-
]
|
|
27
|
-
|
|
28
|
-
export const commonSuffix = ["install"]
|