├── .github
├── FUNDING.yml
└── workflows
│ └── build.yml
├── .gitignore
├── .npmrc
├── .prettierignore
├── .prettierrc
├── CNAME
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── docs
└── assets
│ ├── Banner.png
│ ├── Demo.gif
│ ├── Demo.json
│ ├── heavy.png
│ └── logo
│ ├── autarky.xcf
│ ├── autarky_128.png
│ ├── autarky_256.png
│ ├── autarky_512.png
│ ├── autarky_64.png
│ └── autarky_full.png
├── jest.config.js
├── package.json
├── src
├── index.ts
├── lib
│ ├── Interfaces.ts
│ ├── __tests__
│ │ ├── removeDir.test.ts
│ │ ├── time.test.ts
│ │ ├── utils.test.ts
│ │ └── validation.test.ts
│ ├── childProcesses
│ │ ├── child_delete.ts
│ │ └── child_find.ts
│ ├── getLocation.ts
│ ├── removeDir.ts
│ ├── time.ts
│ ├── utils.ts
│ └── validation.ts
├── redux
│ ├── __tests__
│ │ ├── ConfigReducer.test.ts
│ │ └── actionTypes.test.ts
│ ├── index.ts
│ ├── reducers
│ │ ├── ConfigReducer.ts
│ │ ├── UIReducer.ts
│ │ └── index.ts
│ └── selectors.ts
└── ui
│ ├── App.tsx
│ ├── components
│ ├── Header
│ │ └── index.tsx
│ ├── LogMessage
│ │ └── index.tsx
│ ├── Table
│ │ └── index.tsx
│ └── TextInput
│ │ └── index.tsx
│ ├── containers
│ └── Interrogator
│ │ ├── Questions.tsx
│ │ └── index.tsx
│ └── renderApp.tsx
├── tsconfig.base.json
├── tsconfig.json
├── webpack.config.js
└── yarn.lock
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | custom: ["https://www.paypal.me/pranshuchittora"]
2 |
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: Node Test & Build
2 |
3 | on: [push]
4 |
5 | jobs:
6 | build:
7 | runs-on: ubuntu-latest
8 |
9 | strategy:
10 | matrix:
11 | node-version: [10.x, 12.x]
12 |
13 | steps:
14 | - uses: actions/checkout@v1
15 | - name: Use Node.js ${{ matrix.node-version }}
16 | uses: actions/setup-node@v1
17 | with:
18 | node-version: ${{ matrix.node-version }}
19 | - name: Install, test and build
20 | run: |
21 | yarn
22 | yarn run test
23 | yarn run build
24 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | dist
3 | build
4 | build-dev
5 |
6 | package-lock.json
7 | yarn-error.log
8 |
9 | *.temp
10 |
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | @pranshuchittora:registry=https://npm.pkg.github.com/
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | /node_modules
2 | /dist
3 | /build
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "trailingComma": "all"
3 | }
4 |
--------------------------------------------------------------------------------
/CNAME:
--------------------------------------------------------------------------------
1 | autarky.js.org
2 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | ## Let's Collab 🤘
2 |
3 | ---
4 |
5 | Thank you very much for coming this far.
6 |
7 | ### Up & Running
8 |
9 | - #### Fork
10 |
11 | - #### Clone
12 |
13 | ```
14 | git clone https://github.com/{YOUR_USERNAME}/autarky.git
15 | ```
16 |
17 | - #### Install deps _(Prefer `yarn`)_
18 |
19 | ```bash
20 | # npm
21 | npm i
22 |
23 | # yarn
24 | yarn
25 | ```
26 |
27 | - #### Start dev mode
28 |
29 | ```bash
30 | # npm
31 | npm run watch
32 |
33 | # yarn
34 | yarn run watch
35 | ```
36 |
37 | - #### Running the code
38 |
39 | ```bash
40 | # Development build
41 | node /dist/index.js
42 |
43 | # Production Build
44 | node /build/index.js
45 | ```
46 |
47 | - #### Testing
48 | We are using `jest` as a testing frammework
49 |
50 | ```bash
51 | # npm
52 | npm run test
53 |
54 | # yarn
55 | yarn run test
56 | ```
57 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Pranshu Chittora
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |

3 |
4 |
5 | 
6 | 
7 | 
8 | [](https://dependabot.com/)
9 | [](https://codeclimate.com/github/pranshuchittora/autarky/maintainability)
10 | 
11 |
12 |
13 |
14 |

15 |
16 |
17 | ## Installation
18 |
19 | ```bash
20 | # npm
21 | npm i -g autarky
22 |
23 | #yarn
24 | yarn global add autarky
25 | ```
26 |
27 | ## Usage
28 |
29 | ```bash
30 | $> autarky
31 | ```
32 |
33 | ## Why autarky
34 |
35 | In today's world storage is comparatively costlier than compute. Majority of devs uses MacBooks and sadly MacBooks have pretty low storage (for base models). Hence filling up storage is quite often and we spend a lot of time picking stuff to be deleted.
36 |
37 | ### Motivation
38 |
39 | It's 2019 and I got ran out of storage in my laptop after a thorough analysis I found out that the majority of the storage is occupied by `node_modules`. As each project have a separate node_modules (duplication despite the same version).
40 |
41 | I also have a few projects which I touch once in a blue moon, hence they end up eating a lot of space. On the other hand, picking & removing `node_modules` manually is a tedious process. So I thought why not automate it.
42 |
43 |
44 |

45 |
46 |
47 | ### How it works
48 |
49 | Autarky works by traversing all the child directories recursively relative to the current working directory (the place where you are executing autarky).
50 |
51 | 1. Enter the time in months. Node modules older than the given time will be shown.
52 | 2. Select the `node_modules` which you want to delete.
53 | 3. Confirm deletion.
54 | 4. Done! (No need to pay for more storage.)
55 |
56 | ---
57 |
58 | ## Internals
59 |
60 | Autarky is built with the latest open source technologies.
61 |
62 | 1. UI - The user interface is written in React. Using the Ink's reconciler for rendering the react components.
63 | 2. State Management - The challenge of sharing data b/w UI and the process is achieved using Redux.
64 | 3. Heavy Computation - Large data crunching is done on child processes.
65 |
66 | ### Building Blocks
67 |
68 | - [React](https://reactjs.org/)
69 | - [Redux](https://redux.js.org/)
70 | - [Ink](https://github.com/vadimdemedes/ink)
71 | - [moment](https://momentjs.com/)
72 | - [rimraf](https://github.com/isaacs/rimraf)
73 | - [chalk](https://github.com/chalk/chalk)
74 |
75 | ---
76 |
77 | Read [CONTRIBUTING Guide](./CONTRIBUTING.md)
78 |
79 | License MIT
80 |
81 | Author: Pranshu Chittora
82 |
83 | [Github](https://github.com/pranshuchittora/)
84 | [Twitter](https://twitter.com/pranshuchittora)
85 | [LinkedIn](https://www.linkedin.com/in/pranshuchittora/)
86 |
--------------------------------------------------------------------------------
/docs/assets/Banner.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pranshuchittora/autarky/9a2aa110ba2459e3e58cec104c4be59175a6b573/docs/assets/Banner.png
--------------------------------------------------------------------------------
/docs/assets/Demo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pranshuchittora/autarky/9a2aa110ba2459e3e58cec104c4be59175a6b573/docs/assets/Demo.gif
--------------------------------------------------------------------------------
/docs/assets/heavy.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pranshuchittora/autarky/9a2aa110ba2459e3e58cec104c4be59175a6b573/docs/assets/heavy.png
--------------------------------------------------------------------------------
/docs/assets/logo/autarky.xcf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pranshuchittora/autarky/9a2aa110ba2459e3e58cec104c4be59175a6b573/docs/assets/logo/autarky.xcf
--------------------------------------------------------------------------------
/docs/assets/logo/autarky_128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pranshuchittora/autarky/9a2aa110ba2459e3e58cec104c4be59175a6b573/docs/assets/logo/autarky_128.png
--------------------------------------------------------------------------------
/docs/assets/logo/autarky_256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pranshuchittora/autarky/9a2aa110ba2459e3e58cec104c4be59175a6b573/docs/assets/logo/autarky_256.png
--------------------------------------------------------------------------------
/docs/assets/logo/autarky_512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pranshuchittora/autarky/9a2aa110ba2459e3e58cec104c4be59175a6b573/docs/assets/logo/autarky_512.png
--------------------------------------------------------------------------------
/docs/assets/logo/autarky_64.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pranshuchittora/autarky/9a2aa110ba2459e3e58cec104c4be59175a6b573/docs/assets/logo/autarky_64.png
--------------------------------------------------------------------------------
/docs/assets/logo/autarky_full.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pranshuchittora/autarky/9a2aa110ba2459e3e58cec104c4be59175a6b573/docs/assets/logo/autarky_full.png
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | preset: "ts-jest",
3 | testEnvironment: "node",
4 | globals: {
5 | "ts-jest": {
6 | diagnostics: false,
7 | },
8 | },
9 | roots: ["/src"],
10 | testMatch: [
11 | "**/__tests__/**/*.+(ts|tsx|js)",
12 | "**/?(*.)+(spec|test).+(ts|tsx|js)",
13 | ],
14 | transform: {
15 | "^.+\\.(ts|tsx)?$": "ts-jest",
16 | },
17 | moduleNameMapper: {
18 | "@app/(.*)": "/src/$1",
19 | },
20 | };
21 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "autarky",
3 | "version": "2.0.0",
4 | "description": "Liberating disk space from node_modules",
5 | "author": "Pranshu Chittora ",
6 | "license": "MIT",
7 | "homepage": "https://autarky.js.org",
8 | "repository": {
9 | "type": "git",
10 | "url": "git://github.com/pranshuchittora/autarky.git"
11 | },
12 | "bugs": {
13 | "url": "https://github.com/pranshuchittora/autarky/issues"
14 | },
15 | "bin": {
16 | "autarky": "build/index.js"
17 | },
18 | "main": "build/index",
19 | "types": "build/index.d.ts",
20 | "dependencies": {
21 | "chalk": "5.3.0",
22 | "fs": "^0.0.1-security",
23 | "g-factor": "1.1.0",
24 | "ink": "3.0.8",
25 | "ink-big-text": "1.2.0",
26 | "ink-gradient": "2.0.0",
27 | "ink-multi-select": "2.0.0",
28 | "ink-spinner": "4.0.1",
29 | "ink-table": "3.0.0",
30 | "ink-text-input": "4.0.1",
31 | "log-symbols": "4.0.0",
32 | "moment": "2.24.0",
33 | "react": "17.0.1",
34 | "redux": "4.2.0",
35 | "rimraf": "3.0.0",
36 | "yn": "5.0.0"
37 | },
38 | "scripts": {
39 | "build": "NODE_ENV='production' npx webpack --config webpack.config.js",
40 | "format": "prettier --write 'src/**/*.*'",
41 | "test": "jest",
42 | "prepublish": "yarn run build",
43 | "watch": "NODE_ENV='development' webpack --config webpack.config.js",
44 | "watch:tsc": "tsc -p tsconfig.json --watch",
45 | "watch:test": "jest --watchAll"
46 | },
47 | "devDependencies": {
48 | "@types/chalk": "2.2.0",
49 | "@types/inquirer": "6.5.0",
50 | "@types/jest": "26.0.18",
51 | "@types/moment": "2.13.0",
52 | "@types/node": "14.14.13",
53 | "@types/react": "17.0.0",
54 | "@types/react-redux": "7.1.11",
55 | "@types/redux": "3.6.0",
56 | "@types/rimraf": "2.0.3",
57 | "jest": "29.1.2",
58 | "prettier": "1.19.1",
59 | "ts-jest": "29.0.3",
60 | "ts-loader": "8.0.12",
61 | "tsconfig-paths-webpack-plugin": "3.3.0",
62 | "typescript": "4.1.2",
63 | "webpack": "5.10.1",
64 | "webpack-cli": "4.2.0",
65 | "webpack-node-externals": "2.5.2"
66 | },
67 | "files": [
68 | "build",
69 | "!**/__tests__"
70 | ],
71 | "keywords": [
72 | "cli",
73 | "typescript",
74 | "nodejs",
75 | "node_modules",
76 | "storage manager",
77 | "disk space"
78 | ]
79 | }
80 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | import InitApp from "@app/ui/renderApp";
2 |
3 | (async function() {
4 | // Init React App
5 | InitApp();
6 | setTimeout(() => {}, 20000);
7 | })();
8 |
--------------------------------------------------------------------------------
/src/lib/Interfaces.ts:
--------------------------------------------------------------------------------
1 | export interface IPromptSelect {
2 | name: string;
3 | label: string;
4 | value: string;
5 | size: number;
6 | size_label: string;
7 | time_label: string;
8 | }
9 |
10 | export interface IRefinedListItem {
11 | path: string;
12 | age: any;
13 | }
14 |
--------------------------------------------------------------------------------
/src/lib/__tests__/removeDir.test.ts:
--------------------------------------------------------------------------------
1 | import * as fs from "fs";
2 | import { removeDir, removeDirBulk } from "../removeDir";
3 |
4 | describe("Removes a single file", () => {
5 | test("Removes a file single.temp", () => {
6 | //Remove a file
7 | fs.writeFileSync("single.temp", '');
8 | removeDir("single.temp");
9 | const currentDirArr = fs.readdirSync("./");
10 | expect(currentDirArr.includes("single.temp")).toBe(false);
11 | });
12 | });
13 |
14 | describe("Removes files given an array of path", () => {
15 | test("Removes multiple files", () => {
16 | //Remove a file
17 | fs.writeFileSync("single1.temp", '');
18 | fs.writeFileSync("single2.temp", '');
19 | removeDirBulk(["single1.temp", "single2.temp"]);
20 | const currentDirArr = fs.readdirSync("./");
21 | expect(
22 | currentDirArr.includes("single1.temp") &&
23 | currentDirArr.includes("single2.temp"),
24 | ).toBe(false);
25 | });
26 | });
27 |
--------------------------------------------------------------------------------
/src/lib/__tests__/time.test.ts:
--------------------------------------------------------------------------------
1 | import { TimeRelative } from "../time";
2 |
3 | test("Get realitive time string", () => {
4 | expect(TimeRelative(new Date())).toBe("a few seconds");
5 | });
6 |
--------------------------------------------------------------------------------
/src/lib/__tests__/utils.test.ts:
--------------------------------------------------------------------------------
1 | import { sortQueriesRefinedPath, findTotalSize } from "../utils";
2 |
3 | describe("Tests for utils - sortQueriesRefinedPath", () => {
4 | test("Sort path list based on file age", () => {
5 | const UnsortedList = [
6 | {
7 | age: 200,
8 | path: "file1",
9 | },
10 | {
11 | age: 100,
12 | path: "file2",
13 | },
14 | ];
15 | expect(sortQueriesRefinedPath(UnsortedList)).toStrictEqual([
16 | {
17 | age: 100,
18 | path: "file2",
19 | },
20 | {
21 | age: 200,
22 | path: "file1",
23 | },
24 | ]);
25 | });
26 | });
27 |
28 | describe("Find total size of all dirs", () => {
29 | test("Given an array of object ({size}), find the total size", () => {
30 | const SampleArr = [{ size: 1 }, { size: 2 }];
31 | const TOTAL_SIZE = findTotalSize(SampleArr);
32 |
33 | expect(TOTAL_SIZE).toBe(3);
34 | });
35 | });
36 |
--------------------------------------------------------------------------------
/src/lib/__tests__/validation.test.ts:
--------------------------------------------------------------------------------
1 | import { IntegerValidation } from "../validation";
2 |
3 | describe("utils/IntegerValidation", () => {
4 | test("onChange", () => {
5 | const { onChange } = IntegerValidation;
6 | expect(onChange("ABCD1234EFG567")).toEqual("1234567");
7 | expect(onChange("ABCD1234EFG567")).not.toEqual("ABCD1234EFG567");
8 | });
9 |
10 | test("onDone", () => {
11 | const { onDone } = IntegerValidation;
12 | expect(onDone("1234567")).toEqual(1234567);
13 | expect(onDone("ABCD1234EFG567")).toEqual(NaN);
14 | });
15 | });
--------------------------------------------------------------------------------
/src/lib/childProcesses/child_delete.ts:
--------------------------------------------------------------------------------
1 | import { removeDirBulk } from "@app/lib/removeDir";
2 |
3 | const RemoveDirectories = Resolved_Path_List => {
4 | removeDirBulk(Resolved_Path_List);
5 | };
6 |
7 | process.on("message", message => {
8 | switch (message.type) {
9 | case "START":
10 | const resp = RemoveDirectories(message.payload);
11 | process.send({ type: "DONE", payload: null });
12 | break;
13 | }
14 | });
15 |
--------------------------------------------------------------------------------
/src/lib/childProcesses/child_find.ts:
--------------------------------------------------------------------------------
1 | import { sortQueriesRefinedPath, promptListParser } from "@app/lib/utils";
2 | import { FSSearch } from "@app/lib/getLocation";
3 |
4 | const StartIndexing = FILE_AGE => {
5 | const FSSearchInst = new FSSearch(FILE_AGE);
6 | const QueriedPathList = FSSearchInst.showFiles(process.cwd(), {
7 | filelist: [],
8 | RefinedFileList: [],
9 | });
10 |
11 | if (QueriedPathList.RefinedFileList.length > 0) {
12 | QueriedPathList.RefinedFileList = sortQueriesRefinedPath(
13 | QueriedPathList.RefinedFileList,
14 | );
15 | return promptListParser(QueriedPathList.RefinedFileList);
16 | }
17 | return [];
18 | };
19 |
20 | process.on("message", message => {
21 | switch (message.type) {
22 | case "START":
23 | const resp = StartIndexing(message.payload);
24 | process.send({ type: "DONE", payload: resp });
25 | break;
26 | }
27 | });
28 |
--------------------------------------------------------------------------------
/src/lib/getLocation.ts:
--------------------------------------------------------------------------------
1 | import * as fs from "fs";
2 | import * as path from "path";
3 | import getSize from "g-factor";
4 |
5 | import { validDiff } from "@app/lib/utils";
6 | import { IRefinedListItem } from "@app/lib/Interfaces";
7 | export class FSSearch {
8 | FILE_AGE;
9 | constructor(FILE_AGE) {
10 | this.FILE_AGE = FILE_AGE;
11 | }
12 | showFiles = (dir, { filelist, RefinedFileList }) => {
13 | const self = this;
14 | RefinedFileList = RefinedFileList || [];
15 | filelist = filelist || [];
16 | let files;
17 | try {
18 | files = fs.readdirSync(dir);
19 | } catch (e) {
20 | // console.log("ERR_ACCESS_DENIED", dir);
21 | return { filelist, RefinedFileList };
22 | }
23 | files.forEach(function(file) {
24 | try {
25 | const fileStats = fs.statSync(dir + "/" + file);
26 | const absPath = path.resolve(dir + "/" + file);
27 | if (fileStats.isDirectory() && absPath.includes("/.")) {
28 | return { filelist, RefinedFileList };
29 | }
30 | if (fileStats.isDirectory() && file === "node_modules") {
31 | const fileMTime: any = fileStats.mtime;
32 | const timeCurrent: any = new Date();
33 | const timeDiff = timeCurrent - fileMTime;
34 | // Dir is valid as per the config
35 | if (validDiff(timeDiff, self.FILE_AGE)) {
36 | let fileDetailsObj: IRefinedListItem = {
37 | path: absPath,
38 | age: fileMTime,
39 | };
40 | // console.log(fileDetailsObj);
41 | RefinedFileList.push(fileDetailsObj);
42 | }
43 | } else if (fileStats.isDirectory()) {
44 | filelist = self.showFiles(dir + "/" + file, {
45 | filelist,
46 | RefinedFileList,
47 | }).filelist;
48 | }
49 | } catch (e) {
50 | // console.log("ERR_LOCATION_NOT_FOUND", dir);
51 | }
52 | });
53 | return { filelist, RefinedFileList };
54 | };
55 | }
56 |
--------------------------------------------------------------------------------
/src/lib/removeDir.ts:
--------------------------------------------------------------------------------
1 | import rimraf = require("rimraf");
2 |
3 | export function removeDir(pathToDir: any) {
4 | rimraf.sync(pathToDir);
5 | }
6 |
7 | export function removeDirBulk(DirList: String[]): void {
8 | DirList.forEach(absPath => {
9 | removeDir(absPath);
10 | });
11 | }
12 |
--------------------------------------------------------------------------------
/src/lib/time.ts:
--------------------------------------------------------------------------------
1 | import moment = require("moment");
2 |
3 | /**
4 | *
5 | * @param timeVal
6 | */
7 | export function TimeRelative(timeVal): string {
8 | const timeNow = new Date().getTime();
9 | const timeDiff = timeNow - timeVal;
10 | return moment(timeVal).fromNow(true);
11 | }
12 |
13 | /**
14 | *
15 | * Converts time in months to milliseconds
16 | *
17 | * @param {Number} timeMonths - Time in months
18 | * @returns {Number} timeMilli - Return time in milliSeconds
19 | */
20 |
21 | export function TimeMonthToMilli(time: any): number {
22 | return time * 2.628e9;
23 | }
24 |
--------------------------------------------------------------------------------
/src/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import path from "path";
2 | import chalk from "chalk";
3 | import getSize from "g-factor";
4 |
5 | import { TimeRelative } from "@app/lib/time";
6 | import { TimeMonthToMilli } from "@app/lib/time";
7 | import { IPromptSelect, IRefinedListItem } from "@app/lib/Interfaces";
8 |
9 | /**
10 | *
11 | * @param timeVal
12 | */
13 | export function validDiff(timeVal, FILE_AGE) {
14 | const MinTimeThreshold = TimeMonthToMilli(FILE_AGE);
15 | return timeVal >= MinTimeThreshold ? true : false;
16 | }
17 |
18 | /**
19 | *
20 | * @param List
21 | */
22 | export function promptListParser(List: Object[]): Object[] {
23 | let ParsedList: Object[] = [];
24 |
25 | List.forEach((item: IRefinedListItem) => {
26 | const FileSize = getSize(item.path);
27 | let ItemObj: IPromptSelect = {
28 | name: path.relative(process.cwd(), item.path),
29 | value: item.path,
30 | size: FileSize.SIZE_Number,
31 | label:
32 | " " +
33 | path.relative(process.cwd(), item.path) +
34 | " - " +
35 | chalk.magentaBright(FileSize.SIZE_Parsed) +
36 | " " +
37 | chalk.cyanBright(TimeRelative(item.age) + " old"),
38 | size_label: FileSize.SIZE_Parsed,
39 | time_label: TimeRelative(item.age) + " old",
40 | };
41 | ParsedList.push(ItemObj);
42 | });
43 |
44 | return ParsedList;
45 | }
46 |
47 | export function sortQueriesRefinedPath(RefinedFileList: IRefinedListItem[]) {
48 | RefinedFileList.sort(function(a: IRefinedListItem, b: IRefinedListItem) {
49 | const keyA = a.age;
50 | const keyB = b.age;
51 | // Compares the 2 dates
52 | if (keyA < keyB) return -1;
53 | if (keyA > keyB) return 1;
54 | return 0;
55 | });
56 | return RefinedFileList;
57 | }
58 |
59 | export function findTotalSize(InputArr: IPromptSelect[] | any[]): number {
60 | let sum = 0;
61 | InputArr.forEach(eObj => {
62 | sum += eObj.size;
63 | });
64 | return sum;
65 | }
66 |
--------------------------------------------------------------------------------
/src/lib/validation.ts:
--------------------------------------------------------------------------------
1 | export const IntegerValidation = {
2 | onChange: (input: string): string => {
3 | return input.replace(/\D/g, "");
4 | },
5 | onDone: (input: string): number => {
6 | return parseInt(input);
7 | },
8 | };
9 |
--------------------------------------------------------------------------------
/src/redux/__tests__/ConfigReducer.test.ts:
--------------------------------------------------------------------------------
1 | import store from "@app/redux/index";
2 | import { CHANGE_AGE_CAP, UPDATE_DIRS_LIST } from "@app/redux/reducers/ConfigReducer";
3 |
4 | describe("Tests the Config Reducer - CHANGE_AGE_CAP", () => {
5 | test("Changes the AGE", () => {
6 | store.dispatch({ type: CHANGE_AGE_CAP, payload: { file_age: 5 } });
7 | expect(store.getState().config.file_age).toBe(5);
8 | });
9 | test("Invalid input to the AGE", () => {
10 | const PREV_VALUE = store.getState().config.file_age;
11 | store.dispatch({
12 | type: CHANGE_AGE_CAP,
13 | payload: { file_age: "SomeString" },
14 | });
15 | expect(store.getState().config.file_age).toBe(PREV_VALUE);
16 | });
17 | });
18 |
19 | describe("Tests the Config Reducer - UPDATE_DIRS_LIST", () => {
20 | const FILE_ARR: String[] = ["file1", "file2"];
21 | test("Changes the DIR_LIST", () => {
22 | store.dispatch({
23 | type: UPDATE_DIRS_LIST,
24 | payload: {
25 | dir_list: FILE_ARR,
26 | },
27 | });
28 | expect(store.getState().config.dir_list).toBe(FILE_ARR);
29 | });
30 | });
31 |
--------------------------------------------------------------------------------
/src/redux/__tests__/actionTypes.test.ts:
--------------------------------------------------------------------------------
1 | import {
2 | CHANGE_AGE_CAP,
3 | UPDATE_CONFIRMATION,
4 | UPDATE_DIRS_LIST,
5 | } from "@app/redux/reducers/ConfigReducer";
6 |
7 | const actionTypes = {
8 | CHANGE_AGE_CAP,
9 | UPDATE_CONFIRMATION,
10 | UPDATE_DIRS_LIST,
11 | };
12 |
13 | describe("Action types", () => {
14 | test("Action types must be of type string", () => {
15 | Object.keys(actionTypes).forEach(key => {
16 | expect(typeof actionTypes[key]).toBe("string");
17 | });
18 | });
19 | });
20 |
--------------------------------------------------------------------------------
/src/redux/index.ts:
--------------------------------------------------------------------------------
1 | import { createStore, Store } from "redux";
2 |
3 | import RootReducer from "@app/redux/reducers/index";
4 |
5 | let store: Store = createStore(RootReducer);
6 |
7 | // store.subscribe(() =>
8 | // console.log("Store ->", JSON.stringify(store.getState(), null, 2))
9 | // );
10 |
11 | export default store;
12 |
--------------------------------------------------------------------------------
/src/redux/reducers/ConfigReducer.ts:
--------------------------------------------------------------------------------
1 | const CHANGE_AGE_CAP = "CHANGE_AGE_CAP";
2 | const UPDATE_DIRS_LIST = "UPDATE_DIRS";
3 | const UPDATE_CONFIRMATION = "UPDATE_CONFIRMATION";
4 |
5 | export { CHANGE_AGE_CAP, UPDATE_DIRS_LIST, UPDATE_CONFIRMATION };
6 |
7 | const InitialState = { file_age: null, dir_list: null, confirmation: null };
8 |
9 | export const R_Config = (
10 | state = InitialState,
11 | action: { payload: Object | any; type: String },
12 | ) => {
13 | let newState = { ...state };
14 | const payload = action.payload;
15 |
16 | switch (action.type) {
17 | case CHANGE_AGE_CAP:
18 | if (!isNaN(payload.file_age)) {
19 | newState.file_age = payload.file_age;
20 | }
21 | break;
22 | case UPDATE_DIRS_LIST:
23 | newState.dir_list = payload.dir_list;
24 | break;
25 |
26 | case UPDATE_CONFIRMATION:
27 | newState.confirmation = payload;
28 | break;
29 | }
30 | return newState;
31 | };
32 |
--------------------------------------------------------------------------------
/src/redux/reducers/UIReducer.ts:
--------------------------------------------------------------------------------
1 | const InitialState = {
2 | logs: [],
3 | };
4 |
5 | export const R_UI = (
6 | state = InitialState,
7 | action: { payload: Object | any; type: String },
8 | ) => {
9 | let newState = { ...state };
10 |
11 | const payload = action.payload;
12 |
13 | switch (action.type) {
14 | case APPEND_LOGS:
15 | newState.logs.push(payload);
16 | break;
17 | }
18 | return newState;
19 | };
20 |
21 | export const APPEND_LOGS = "UI/Append_Logs";
22 | // const APPEND_LOGS = "UI/Append_Logs"
23 |
--------------------------------------------------------------------------------
/src/redux/reducers/index.ts:
--------------------------------------------------------------------------------
1 | import { combineReducers } from "redux";
2 |
3 | import { R_Config } from "@app/redux/reducers/ConfigReducer";
4 | import { R_UI } from "@app/redux/reducers/UIReducer";
5 |
6 | const RootReducer = combineReducers({
7 | config: R_Config,
8 | UI: R_UI,
9 | });
10 |
11 | export default RootReducer;
12 |
--------------------------------------------------------------------------------
/src/redux/selectors.ts:
--------------------------------------------------------------------------------
1 | import Store from "@app/redux/index";
2 |
3 | export const SelectFileAge = (store = Store.getState()) => {
4 | return store.config.file_age;
5 | };
6 | export const SelectLogs = (store = Store.getState()) => {
7 | return store.UI.logs;
8 | };
9 |
10 | export const SelectDirList = (store = Store.getState()) => {
11 | return store.config.dir_list;
12 | };
13 | export const SelectConfirmation = (store = Store.getState()) => {
14 | return store.config.confirmation;
15 | };
16 |
--------------------------------------------------------------------------------
/src/ui/App.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import { Box } from "ink";
4 | import Header from "@app/ui/components/Header";
5 |
6 | import Interrogator from "@app/ui/containers/Interrogator/index";
7 |
8 | const App = () => {
9 | return (
10 | <>
11 |
12 |
13 |
14 |
15 | >
16 | );
17 | };
18 |
19 | export default App;
20 |
--------------------------------------------------------------------------------
/src/ui/components/Header/index.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { render, Box, Text } from "ink";
3 | import Gradient from "ink-gradient";
4 | import BigText from "ink-big-text";
5 |
6 | const Header: React.FunctionComponent = () => {
7 | return (
8 |
9 |
10 |
11 |
12 |
13 | );
14 | };
15 |
16 | export default Header;
17 |
--------------------------------------------------------------------------------
/src/ui/components/LogMessage/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect, useCallback, useMemo } from "react";
2 | import { Box, Text } from "ink";
3 |
4 | interface ILogMessageProps {
5 | logSymbol: string;
6 | label: string;
7 | value: string;
8 | }
9 |
10 | const LogMessage: React.FunctionComponent = props => {
11 | return (
12 | <>
13 |
14 |
15 | {props.logSymbol}
16 |
17 |
18 |
19 | {`${props.label} ${
20 | props.value ? ": " + props.value : ""
21 | }`}
22 |
23 |
24 | >
25 | );
26 | };
27 |
28 | export default LogMessage;
29 |
--------------------------------------------------------------------------------
/src/ui/components/Table/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useMemo } from "react";
2 | import InkTable from "ink-table";
3 |
4 | interface ITableProps {
5 | data: any[];
6 | }
7 |
8 | const Table: React.FunctionComponent = props => {
9 | const data = useMemo(() => {
10 | const newData = [...props.data];
11 | return newData.map(obj => {
12 | return {
13 | Location: obj.value,
14 | Size: obj.size_label,
15 | Time: obj.time_label,
16 | };
17 | });
18 | }, [props.data]);
19 | return ;
20 | };
21 |
22 | export default Table;
23 |
--------------------------------------------------------------------------------
/src/ui/components/TextInput/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import { Box, Text } from "ink";
3 | import Spinner from "ink-spinner";
4 | import InkTextInput from "ink-text-input";
5 |
6 | interface ITextInputProps {
7 | onChange: (string) => string;
8 | submit: (string) => void;
9 | label: string;
10 | }
11 |
12 | const TextInput: React.FunctionComponent = props => {
13 | const [query, setQuery] = useState("");
14 | const [submitted, setSubmitted] = useState(false);
15 | const handleChange = (q: string) => {
16 | setQuery(props.onChange(q));
17 | };
18 | const handleSubmit = () => {
19 | setSubmitted(true);
20 | props.submit(query);
21 | };
22 |
23 | if (submitted) {
24 | return null;
25 | }
26 |
27 | return (
28 | <>
29 |
30 |
31 |
32 |
33 |
34 | {props.label}
35 |
36 |
37 |
42 |
43 | >
44 | );
45 | };
46 |
47 | export default TextInput;
48 |
--------------------------------------------------------------------------------
/src/ui/containers/Interrogator/Questions.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect, useCallback, useMemo } from "react";
2 | import chalk from "chalk";
3 | import { convertBytes } from "g-factor";
4 | import yn from "yn";
5 | import { fork } from "child_process";
6 | import path from "path";
7 | import LogSymbols from "log-symbols";
8 | import { Box, Text } from "ink";
9 | import MultiSelect from "ink-multi-select";
10 | import Spinner from "ink-spinner";
11 |
12 | import { IntegerValidation } from "@app/lib/validation";
13 | import { findTotalSize } from "@app/lib/utils";
14 | import store from "@app/redux/index";
15 | import {
16 | CHANGE_AGE_CAP,
17 | UPDATE_DIRS_LIST,
18 | UPDATE_CONFIRMATION,
19 | } from "@app/redux/reducers/ConfigReducer";
20 | import { APPEND_LOGS } from "@app/redux/reducers/UIReducer";
21 | import {
22 | SelectConfirmation,
23 | SelectDirList,
24 | SelectFileAge,
25 | SelectLogs,
26 | } from "@app/redux/selectors";
27 | import TextInput from "@app/ui/components/TextInput";
28 |
29 | export const AgeQuestion: React.FunctionComponent = () => {
30 | const label = "How old node_modules you wanna delete? (months)";
31 |
32 | const handleSubmit = (val: string): void => {
33 | const value = IntegerValidation.onDone(val);
34 |
35 | store.dispatch({ type: CHANGE_AGE_CAP, payload: { file_age: value } });
36 | store.dispatch({
37 | type: APPEND_LOGS,
38 | payload: { logSymbol: LogSymbols.success, label, value, id: "file_age" },
39 | });
40 | };
41 | return (
42 |
47 | );
48 | };
49 |
50 | export const DirSelect: React.FunctionComponent = () => {
51 | const [data, setData] = useState(null);
52 | const [selectedCount, setSelectedCount] = useState(0);
53 | const [selected, setSelected] = useState(null);
54 | const [done, setDone] = useState(false);
55 | useEffect(() => {
56 | const child = fork(path.resolve(__dirname, "child_find.js"));
57 |
58 | child.send({ type: "START", payload: SelectFileAge(store.getState()) });
59 | child.on("error", err => {
60 | console.log("\n\t\tERROR: spawn failed! (" + err + ")");
61 | });
62 | child.on("message", (message: any) => {
63 | const { type, payload } = message;
64 | switch (type) {
65 | case "DONE": {
66 | child.kill();
67 |
68 | store.dispatch({
69 | type: APPEND_LOGS,
70 | payload: {
71 | logSymbol: LogSymbols.success,
72 | label: "Indexing file system.",
73 | value: "",
74 | id: "indexing_fs",
75 | },
76 | });
77 | setDone(true);
78 | if (Array.isArray(payload) && payload.length == 0) {
79 | store.dispatch({
80 | type: APPEND_LOGS,
81 | payload: {
82 | logSymbol: LogSymbols.info,
83 | label: "Oops! Your node_modules are too young to be deleted.",
84 | value: "",
85 | id: "no_dir_found",
86 | },
87 | });
88 | process.exit(0);
89 | }
90 |
91 | setData(payload);
92 | break;
93 | }
94 | case "MESSAGE": {
95 | console.log(payload);
96 | }
97 | }
98 | });
99 | return () => {
100 | !child.killed ? child.kill() : null;
101 | };
102 | }, []);
103 |
104 | const handleSubmit = items => {
105 | setSelected(items);
106 | if (Array.isArray(items) && items.length > 0) {
107 | store.dispatch({
108 | type: UPDATE_DIRS_LIST,
109 | payload: {
110 | dir_list: items,
111 | },
112 | });
113 | }
114 | };
115 |
116 | const handleSelect = () => {
117 | setSelectedCount(selectedCount + 1);
118 | };
119 | const handleUnSelect = () => {
120 | setSelectedCount(selectedCount - 1);
121 | };
122 | const RenderInstructionsAndError = (
123 | <>
124 |
125 | {Array.isArray(selected) && selected.length == 0 ? (
126 | Select atleast one.
127 | ) : (
128 | Select directories to be deleted.
129 | )}
130 |
131 |
137 |
143 |
144 | {"Press to select, to continue"}
145 |
146 |
147 |
153 | {`Selected: ${selectedCount}`}
154 |
155 |
156 | >
157 | );
158 |
159 | if (!done) {
160 | return (
161 |
162 |
163 | Indexing the Disk
164 |
165 | );
166 | }
167 |
168 | return (
169 | <>
170 | {data != null && (
171 |
172 | {RenderInstructionsAndError}
173 |
180 |
181 | )}
182 | >
183 | );
184 | };
185 |
186 | interface IConfirmDeletionProps {
187 | count: number;
188 | }
189 | export const ConfirmDeletion: React.FunctionComponent = props => {
190 | const handleChange = q => {
191 | return q;
192 | };
193 |
194 | const label = `Confirm deleting ${props.count} directories ? (y/n)`;
195 |
196 | const handleSubmit = (val: string): void => {
197 | const Response = yn(val);
198 |
199 | store.dispatch({
200 | type: APPEND_LOGS,
201 | payload: {
202 | logSymbol: Response ? LogSymbols.success : LogSymbols.error,
203 | label,
204 | value: val,
205 | id: "confirm_deletion",
206 | },
207 | });
208 |
209 | if (Response === false) {
210 | process.exit(0);
211 | }
212 |
213 | store.dispatch({
214 | type: UPDATE_CONFIRMATION,
215 | payload: Response,
216 | });
217 | };
218 | return (
219 |
220 | );
221 | };
222 |
223 | export const RemoveDirs = () => {
224 | const [done, setDone] = useState(false);
225 | const [successMessage, setSuccessMessage] = useState("");
226 | useEffect(() => {
227 | const Dir_List = SelectDirList(store.getState());
228 | const Resolved_Path_List = Dir_List.map(e => {
229 | return e.value;
230 | });
231 | const TOTAL_SIZE = findTotalSize(Dir_List);
232 |
233 | const child = fork(path.resolve(__dirname, "child_delete.js"));
234 |
235 | child.send({ type: "START", payload: Resolved_Path_List });
236 | child.on("error", err => {
237 | console.log("\n\t\tERROR: spawn failed! (" + err + ")");
238 | });
239 | child.on("message", (message: any) => {
240 | if (message.type === "DONE") {
241 | child.kill();
242 |
243 | store.dispatch({
244 | type: APPEND_LOGS,
245 | payload: {
246 | logSymbol: LogSymbols.success,
247 | label: `Deleted ${Dir_List?.length} ${
248 | Dir_List?.length > 1 ? "directories" : "directory"
249 | } successfully. `,
250 | value: "",
251 | id: "deleted_dir",
252 | },
253 | });
254 |
255 | setSuccessMessage(
256 | chalk.magentaBright(convertBytes(TOTAL_SIZE)) +
257 | " now free on your 💻",
258 | );
259 | setDone(true);
260 | }
261 | });
262 | return () => {
263 | if (!child.killed) child.kill();
264 | };
265 | }, []);
266 |
267 | if (done) {
268 | return (
269 |
270 | {successMessage}
271 |
272 | );
273 | }
274 | return (
275 |
276 |
277 |
278 |
279 | Deleting directories...
280 |
281 | );
282 | };
283 |
--------------------------------------------------------------------------------
/src/ui/containers/Interrogator/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect, useCallback, useMemo } from "react";
2 |
3 | import {
4 | AgeQuestion,
5 | DirSelect,
6 | ConfirmDeletion,
7 | RemoveDirs,
8 | } from "@app/ui/containers/Interrogator/Questions";
9 | import store from "@app/redux/index";
10 | import {
11 | SelectConfirmation,
12 | SelectDirList,
13 | SelectFileAge,
14 | SelectLogs,
15 | } from "@app/redux/selectors";
16 |
17 | import LogMessage from "@app/ui/components/LogMessage";
18 | import Table from "@app/ui/components/Table";
19 |
20 | const Interrogator: React.FunctionComponent = () => {
21 | const [RStore, setRStore] = useState(store.getState());
22 |
23 | useEffect(() => {
24 | const unsubscribe = store.subscribe(() => {
25 | const newStoreState = store.getState();
26 | setRStore(newStoreState);
27 | });
28 |
29 | return () => {
30 | unsubscribe();
31 | };
32 | }, []);
33 |
34 | let question;
35 | if (SelectFileAge(RStore) === null) {
36 | question = ;
37 | } else if (SelectDirList(RStore) === null) {
38 | question = ;
39 | } else if (SelectConfirmation(RStore) === null) {
40 | question = (
41 | <>
42 |
43 |
44 | >
45 | );
46 | } else {
47 | question = (
48 | <>
49 |
50 |
51 | >
52 | );
53 | }
54 |
55 | return (
56 | <>
57 | {SelectLogs(RStore).map((item: any) => {
58 | return ;
59 | })}
60 | {question}
61 | >
62 | );
63 | };
64 |
65 | export default Interrogator;
66 |
--------------------------------------------------------------------------------
/src/ui/renderApp.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { render } from "ink";
3 |
4 | import App from "./App";
5 |
6 | const RenderApp = () => {
7 | render();
8 | };
9 |
10 | export default RenderApp;
11 |
--------------------------------------------------------------------------------
/tsconfig.base.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "rootDir": "src",
4 | // "allowSyntheticDefaultImports": true,
5 |
6 | "types": ["jest", "react"],
7 | "esModuleInterop": true,
8 | "module": "CommonJS",
9 | "moduleResolution": "node",
10 | "skipLibCheck": true,
11 | "strict": false,
12 | "lib": ["es6", "es2017"],
13 | "resolveJsonModule": true,
14 | // "declaration": true,
15 | "jsx": "react",
16 | "sourceMap": true,
17 | "baseUrl": ".",
18 | "paths": {
19 | "@app/*": ["src/*"]
20 | }
21 | },
22 |
23 | "include": ["src"]
24 | }
25 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.base.json",
3 | "compilerOptions": {
4 | "sourceMap": true,
5 | "outDir": "dist",
6 | "target": "es6",
7 | "esModuleInterop": true,
8 | // "noEmit": true
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require("path");
2 |
3 | const TSConfigPathsPlugin = require("tsconfig-paths-webpack-plugin");
4 | const webpack = require("webpack");
5 | const nodeExternals = require("webpack-node-externals");
6 |
7 | const isProd = process.env.NODE_ENV == "production";
8 |
9 | module.exports = {
10 | mode: process.env.NODE_ENV,
11 |
12 | entry: {
13 | index: "src/index.ts",
14 | child_find: "src/lib/childProcesses/child_find.ts",
15 | child_delete: "src/lib/childProcesses/child_delete.ts",
16 | },
17 | output: {
18 | filename: "[name].js",
19 | path: path.resolve(__dirname, isProd ? "build" : "build-dev"),
20 | },
21 |
22 | watch: !isProd,
23 | watchOptions: {
24 | ignored: "node_modules/**",
25 | },
26 |
27 | devtool: isProd ? "inline-source-map" : false,
28 |
29 | target: "node",
30 |
31 | externals: [nodeExternals()],
32 | plugins: [
33 | new webpack.BannerPlugin({
34 | banner: "#!/usr/bin/env node",
35 | raw: true,
36 | include: "index.js",
37 | }),
38 | ],
39 | module: {
40 | rules: [
41 | {
42 | test: /\.(tsx|ts|js)?$/,
43 | use: [
44 | {
45 | loader: "ts-loader",
46 | options: {
47 | configFile: "tsconfig.json",
48 | },
49 | },
50 | ],
51 | exclude: /node_modules/,
52 | },
53 | ],
54 | },
55 |
56 | resolve: {
57 | plugins: [new TSConfigPathsPlugin({ configFile: "tsconfig.json" })],
58 | extensions: [".tsx", ".ts", ".js"],
59 | alias: {
60 | "@app": path.resolve(__dirname, "src"),
61 | },
62 | },
63 | };
64 |
65 | // #!/usr/bin/env node
66 |
--------------------------------------------------------------------------------