diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..b387581 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,16 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [1.3.0] - 2022-08-01 + +### Added + +- Support for casks. + +### Security + +- Update dependencies. diff --git a/package.json b/package.json index c73872d..011fb6c 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "unbrew", "description": "brew cleanup utility", - "version": "1.2.4", + "version": "1.3.0", "author": { "name": "Niccolo Borgioli", "email": "hi@nicco.io", @@ -32,11 +32,11 @@ "type": "module", "dependencies": { "chalk": "^5.0.1", - "inquirer": "^8.2.1" + "inquirer": "^8.2.4" }, "devDependencies": { - "@types/inquirer": "^8.2.0", - "@types/node": "^16.11.26", - "typescript": "^4.6.2" + "@types/inquirer": "^8.2.1", + "@types/node": "^16.11.47", + "typescript": "^4.7.4" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 94d7e58..f16a7f1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,38 +1,38 @@ -lockfileVersion: 5.3 +lockfileVersion: 5.4 specifiers: - '@types/inquirer': ^8.2.0 - '@types/node': ^16.11.26 + '@types/inquirer': ^8.2.1 + '@types/node': ^16.11.47 chalk: ^5.0.1 - inquirer: ^8.2.1 - typescript: ^4.6.2 + inquirer: ^8.2.4 + typescript: ^4.7.4 dependencies: chalk: 5.0.1 - inquirer: 8.2.1 + inquirer: 8.2.4 devDependencies: - '@types/inquirer': 8.2.0 - '@types/node': 16.11.26 - typescript: 4.6.2 + '@types/inquirer': 8.2.1 + '@types/node': 16.11.47 + typescript: 4.7.4 packages: - /@types/inquirer/8.2.0: - resolution: {integrity: sha512-BNoMetRf3gmkpAlV5we+kxyZTle7YibdOntIZbU5pyIfMdcwy784KfeZDAcuyMznkh5OLa17RVXZOGA5LTlkgQ==} + /@types/inquirer/8.2.1: + resolution: {integrity: sha512-wKW3SKIUMmltbykg4I5JzCVzUhkuD9trD6efAmYgN2MrSntY0SMRQzEnD3mkyJ/rv9NLbTC7g3hKKE86YwEDLw==} dependencies: '@types/through': 0.0.30 - rxjs: 7.5.5 + rxjs: 7.5.6 dev: true - /@types/node/16.11.26: - resolution: {integrity: sha512-GZ7bu5A6+4DtG7q9GsoHXy3ALcgeIHP4NnL0Vv2wu0uUB/yQex26v0tf6/na1mm0+bS9Uw+0DFex7aaKr2qawQ==} + /@types/node/16.11.47: + resolution: {integrity: sha512-fpP+jk2zJ4VW66+wAMFoBJlx1bxmBKx4DUFf68UHgdGCOuyUTDlLWqsaNPJh7xhNDykyJ9eIzAygilP/4WoN8g==} dev: true /@types/through/0.0.30: resolution: {integrity: sha512-FvnCJljyxhPM3gkRgWmxmDZyAQSiBQQWLI0A0VFL0K7W1oRUrPJSqNO0NvTnLkBcotdlp3lKvaT0JrnyRDkzOg==} dependencies: - '@types/node': 16.11.26 + '@types/node': 16.11.47 dev: true /ansi-escapes/4.3.2: @@ -97,8 +97,8 @@ packages: restore-cursor: 3.1.0 dev: false - /cli-spinners/2.6.1: - resolution: {integrity: sha512-x/5fWmGMnbKQAaNwN+UZlV79qBLM9JFnJuJ03gIi5whrob0xV0ofNVHy9DhwGdsMJQc2OKv0oGmLzvaqvAVv+g==} + /cli-spinners/2.7.0: + resolution: {integrity: sha512-qu3pN8Y3qHNgE2AFweciB1IfMnmZ/fsNTEE+NOFjmGB2F/7rLhnhzppvpCnN4FovtP26k8lHyy9ptEbNwWFLzw==} engines: {node: '>=6'} dev: false @@ -108,7 +108,7 @@ packages: dev: false /clone/1.0.4: - resolution: {integrity: sha1-2jCcwmPfFZlMaIypAheco8fNfH4=} + resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} engines: {node: '>=0.8'} dev: false @@ -124,7 +124,7 @@ packages: dev: false /defaults/1.0.3: - resolution: {integrity: sha1-xlYFHpgX2f8I7YgUd/P+QBnz730=} + resolution: {integrity: sha512-s82itHOnYrN0Ib8r+z7laQz3sdE+4FP3d9Q7VLO7U+KRT+CR0GsWuyHxzdAY82I7cXv0G/twrqomTJLOssO5HA==} dependencies: clone: 1.0.4 dev: false @@ -134,7 +134,7 @@ packages: dev: false /escape-string-regexp/1.0.5: - resolution: {integrity: sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=} + resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} engines: {node: '>=0.8.0'} dev: false @@ -174,8 +174,8 @@ packages: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} dev: false - /inquirer/8.2.1: - resolution: {integrity: sha512-pxhBaw9cyTFMjwKtkjePWDhvwzvrNGAw7En4hottzlPvz80GZaMZthdDU35aA6/f5FRZf3uhE057q8w1DE3V2g==} + /inquirer/8.2.4: + resolution: {integrity: sha512-nn4F01dxU8VeKfq192IjLsxu0/OmMZ4Lg3xKAns148rCaXP6ntAoEkVYZThWjwON8AlzdZZi6oqnhNbxUG9hVg==} engines: {node: '>=12.0.0'} dependencies: ansi-escapes: 4.3.2 @@ -188,10 +188,11 @@ packages: mute-stream: 0.0.8 ora: 5.4.1 run-async: 2.4.1 - rxjs: 7.5.5 + rxjs: 7.5.6 string-width: 4.2.3 strip-ansi: 6.0.1 through: 2.3.8 + wrap-ansi: 7.0.0 dev: false /is-fullwidth-code-point/3.0.0: @@ -244,7 +245,7 @@ packages: bl: 4.1.0 chalk: 4.1.2 cli-cursor: 3.1.0 - cli-spinners: 2.6.1 + cli-spinners: 2.7.0 is-interactive: 1.0.0 is-unicode-supported: 0.1.0 log-symbols: 4.1.0 @@ -253,7 +254,7 @@ packages: dev: false /os-tmpdir/1.0.2: - resolution: {integrity: sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=} + resolution: {integrity: sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==} engines: {node: '>=0.10.0'} dev: false @@ -279,10 +280,10 @@ packages: engines: {node: '>=0.12.0'} dev: false - /rxjs/7.5.5: - resolution: {integrity: sha512-sy+H0pQofO95VDmFLzyaw9xNJU4KTRSwQIGM6+iG3SypAtCiLDzpeG8sJrNCWn2Up9km+KhkvTdbkrdy+yzZdw==} + /rxjs/7.5.6: + resolution: {integrity: sha512-dnyv2/YsXhnm461G+R/Pe5bWP41Nm6LBXEYWI6eiFP4fiwx6WRI/CD0zbdVAudd9xwLEF2IDcKXLHit0FYjUzw==} dependencies: - tslib: 2.3.1 + tslib: 2.4.0 /safe-buffer/5.2.1: resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} @@ -326,7 +327,7 @@ packages: dev: false /through/2.3.8: - resolution: {integrity: sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=} + resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} dev: false /tmp/0.0.33: @@ -336,26 +337,35 @@ packages: os-tmpdir: 1.0.2 dev: false - /tslib/2.3.1: - resolution: {integrity: sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==} + /tslib/2.4.0: + resolution: {integrity: sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==} /type-fest/0.21.3: resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} engines: {node: '>=10'} dev: false - /typescript/4.6.2: - resolution: {integrity: sha512-HM/hFigTBHZhLXshn9sN37H085+hQGeJHJ/X7LpBWLID/fbc2acUMfU+lGD98X81sKP+pFa9f0DZmCwB9GnbAg==} + /typescript/4.7.4: + resolution: {integrity: sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==} engines: {node: '>=4.2.0'} hasBin: true dev: true /util-deprecate/1.0.2: - resolution: {integrity: sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=} + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} dev: false /wcwidth/1.0.1: - resolution: {integrity: sha1-8LDc+RW8X/FSivrbLA4XtTLaL+g=} + resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==} dependencies: defaults: 1.0.3 dev: false + + /wrap-ansi/7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + dev: false diff --git a/src/index.ts b/src/index.ts index 597d800..82c3b1e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,10 +1,31 @@ #!/usr/bin/env node -import cp from 'child_process' import chalk from 'chalk' +import cp from 'child_process' import inquirer from 'inquirer' -const VERSION = '1.2.3' +const VERSION = '1.3.0' + +class ListEntry { + name!: string + type!: 'package' | 'cask' + + constructor(options: ListEntry) { + Object.assign(this, options) + } + + toString() { + return `${this.type === 'package' ? '📦' : '🍾'} ${this.name}` + } + + static parse(line: string): ListEntry { + const [type, name] = line.split(' ') + if (!name) throw new Error('Could not parse line') + if (type === '📦') return new ListEntry({ name, type: 'package' }) + if (type === '🍾') return new ListEntry({ name, type: 'cask' }) + throw new Error('Could not parse type') + } +} function checkIfBrewIsInstalled() { try { @@ -15,13 +36,30 @@ function checkIfBrewIsInstalled() { } } -function getListOfLeaves(): string[] { +function getListOfPackages(): ListEntry[] { const list = cp.execSync('brew leaves', { encoding: 'utf-8' }) - return list.trim().split('\n') + return list + .trim() + .split('\n') + .map((line) => new ListEntry({ name: line, type: 'package' })) } -function getLoosers(keepers: string[], leaves = getListOfLeaves()): string[] { - return leaves.filter((leave) => !keepers.includes(leave)) +function getListOfCasks(): ListEntry[] { + const list = cp.execSync('brew list --cask -1', { encoding: 'utf-8' }) + return list + .trim() + .split('\n') + .map((line) => new ListEntry({ name: line, type: 'cask' })) +} + +function getList(cask: boolean): ListEntry[] { + const list = getListOfPackages() + if (cask) list.push(...getListOfCasks()) + return list +} + +function getLoosers(keepers: ListEntry[], leaves: ListEntry[]): ListEntry[] { + return leaves.filter((leave) => !keepers.some((keeper) => keeper.name === leave.name)) } async function main() { @@ -31,49 +69,64 @@ async function main() { } console.log(`${chalk.bold.blue('UnBrew')} - Brew cleanup utility\nVersion: ${VERSION}\n`) - let leaves: string[] - let loosers: string[] + const { cask } = await inquirer.prompt([ + { + type: 'confirm', + name: 'cask', + message: `Also consider casks?`, + }, + ]) + + let initialState = getList(cask) - leaves = getListOfLeaves() const { keepers } = await inquirer.prompt([ { type: 'checkbox', message: 'Select packages to keep (all by default)', name: 'keepers', - choices: leaves.map((leave) => ({ - name: leave, + choices: initialState.map((entry) => ({ + name: entry, checked: true, })), }, ]) - loosers = getLoosers(keepers, leaves) - if (loosers.length === 0) { - console.log(chalk.bold('No package/s selected for deletion.')) - return - } - const { confirmed } = await inquirer.prompt([ - { - type: 'confirm', - name: 'confirmed', - message: `Delelte: ${chalk.bold.blue(loosers.join(' '))}`, - }, - ]) + // Uninstalling + let first = true + let allUninstalled: ListEntry[] = [] + while (true) { + // Get all to be uninstalled + const loosers = getLoosers(keepers, first ? initialState : getList(cask)) - if (!confirmed) { - console.log(chalk.bold.red('Aborted')) - return + // First time prompt + if (first) { + if (loosers.length === 0) { + console.log(chalk.bold('No package/s selected for deletion.')) + return + } + const { confirmed } = await inquirer.prompt([ + { + type: 'confirm', + name: 'confirmed', + message: `Delelte: ${chalk.bold.blue(loosers.map((l) => l.name).join(', '))} and their dependencies?`, + }, + ]) + + if (!confirmed) { + console.log(chalk.bold.red('Aborted')) + return + } + console.log('🗑 Uninstalling') + first = false + } + + // Actual uninstalling + if (loosers.length === 0) break + allUninstalled.push(...loosers) + cp.execSync(`brew uninstall ${loosers.map((l) => l.name).join(' ')}`) } - console.log('🗑 Uninstalling') - const allLoosers: string[] = [] - while (loosers.length) { - allLoosers.push(...loosers) - const joinedLoosers = loosers.join(' ') - cp.execSync(`brew uninstall ${joinedLoosers}`) - loosers = getLoosers(keepers) - } - console.log('✅ Uninstalled: ' + allLoosers.join(', ')) + console.log('✅ Uninstalled: ' + allUninstalled.join(', ')) console.log('🧽 Cleaning up') cp.execSync(`brew cleanup`) @@ -81,5 +134,5 @@ async function main() { console.log(chalk.bold.green('🚀 Done')) } main().finally(() => { - console.log(chalk.blue('Bye Bye 👋')) + console.log(chalk.blue('👋 Bye Bye')) })