Showing preview only (534K chars total). Download the full file or copy to clipboard to get everything.
Repository: voidcosmos/npkill
Branch: main
Commit: 064992b889d4
Files: 140
Total size: 497.9 KB
Directory structure:
gitextract_fg_l4s4m/
├── .github/
│ ├── CODE_OF_CONDUCT.es.md
│ ├── CODE_OF_CONDUCT.md
│ ├── CONTRIBUTING.es.md
│ ├── CONTRIBUTING.md
│ ├── FUNDING.yml
│ ├── ISSUE_TEMPLATE/
│ │ ├── bug-report.md
│ │ └── feature_request.md
│ └── workflows/
│ ├── codeql-analysis.yml
│ └── nodejs.yml
├── .gitignore
├── .husky/
│ ├── commit-msg
│ └── pre-commit
├── .node-version
├── .npmignore
├── .prettierrc
├── .vscode/
│ ├── launch.json
│ └── settings.json
├── API.md
├── LICENSE
├── README.es.md
├── README.id.md
├── README.md
├── README.pt.md
├── README.tr.md
├── docs/
│ ├── RELEASE.md
│ ├── create-demo.sh
│ ├── json-output.md
│ ├── npkillrc.md
│ └── profiles.md
├── eslint.config.mjs
├── jest.config.ts
├── package.json
├── src/
│ ├── cli/
│ │ ├── cli.controller.ts
│ │ ├── interfaces/
│ │ │ ├── cli-options.interface.ts
│ │ │ ├── command-keys.interface.ts
│ │ │ ├── config.interface.ts
│ │ │ ├── index.ts
│ │ │ ├── json-output.interface.ts
│ │ │ ├── key-press.interface.ts
│ │ │ ├── node-version.interface.ts
│ │ │ ├── stats.interface.ts
│ │ │ ├── ui-positions.interface.ts
│ │ │ └── version.interface.ts
│ │ ├── models/
│ │ │ └── start-parameters.model.ts
│ │ ├── services/
│ │ │ ├── console.service.ts
│ │ │ ├── https.service.ts
│ │ │ ├── index.ts
│ │ │ ├── json-output.service.ts
│ │ │ ├── results.service.ts
│ │ │ ├── scan.service.ts
│ │ │ ├── spinner.service.ts
│ │ │ ├── ui.service.ts
│ │ │ └── update.service.ts
│ │ └── ui/
│ │ ├── base.ui.ts
│ │ ├── components/
│ │ │ ├── general.ui.ts
│ │ │ ├── header/
│ │ │ │ ├── header-ui.constants.ts
│ │ │ │ ├── header.ui.ts
│ │ │ │ ├── stats.ui.ts
│ │ │ │ └── status.ui.ts
│ │ │ ├── help/
│ │ │ │ ├── help-command.ui.ts
│ │ │ │ ├── help.constants.ts
│ │ │ │ └── help.ui.ts
│ │ │ ├── logs.ui.ts
│ │ │ ├── options.ui.ts
│ │ │ ├── result-details.ui.ts
│ │ │ ├── results.ui.ts
│ │ │ └── warning.ui.ts
│ │ ├── heavy.ui.ts
│ │ └── index.ts
│ ├── constants/
│ │ ├── cli.constants.ts
│ │ ├── index.ts
│ │ ├── main.constants.ts
│ │ ├── messages.constants.ts
│ │ ├── options.constants.ts
│ │ ├── os-service-map.constants.ts
│ │ ├── result-descriptions.constants.ts
│ │ ├── sort.result.ts
│ │ ├── spinner.constants.ts
│ │ ├── status.constants.ts
│ │ ├── update.constants.ts
│ │ └── workers.constants.ts
│ ├── core/
│ │ ├── constants/
│ │ │ ├── global-ignored.constants.ts
│ │ │ ├── index.ts
│ │ │ └── profiles.constants.ts
│ │ ├── index.ts
│ │ ├── interfaces/
│ │ │ ├── file-service.interface.ts
│ │ │ ├── folder.interface.ts
│ │ │ ├── index.ts
│ │ │ ├── logger-service.interface.ts
│ │ │ ├── npkill.interface.ts
│ │ │ ├── npkillrc-config.interface.ts
│ │ │ ├── profile.interface.ts
│ │ │ ├── search-status.model.ts
│ │ │ └── services.interface.ts
│ │ ├── npkill.ts
│ │ └── services/
│ │ ├── config/
│ │ │ ├── config-merger.ts
│ │ │ ├── config-validator.ts
│ │ │ ├── index.ts
│ │ │ ├── profile-validator.ts
│ │ │ └── property-validators.ts
│ │ ├── config.service.ts
│ │ ├── files/
│ │ │ ├── files.service.ts
│ │ │ ├── files.worker.service.ts
│ │ │ ├── files.worker.ts
│ │ │ ├── index.ts
│ │ │ ├── unix-files.service.ts
│ │ │ └── windows-files.service.ts
│ │ ├── index.ts
│ │ ├── logger.service.ts
│ │ ├── profiles.service.ts
│ │ └── stream.service.ts
│ ├── dirname.ts
│ ├── index.ts
│ ├── main.ts
│ └── utils/
│ ├── get-file-content.ts
│ ├── is-safe-to-delete.ts
│ └── unit-conversions.ts
├── stryker.conf.js
├── tests/
│ ├── cli/
│ │ ├── cli.controller.test.ts
│ │ ├── services/
│ │ │ ├── console.service.test.ts
│ │ │ ├── https.service.test.ts
│ │ │ ├── json-output.service.test.ts
│ │ │ ├── profiles.service.test.ts
│ │ │ ├── result.service.test.ts
│ │ │ ├── scan.service.test.ts
│ │ │ ├── spinner.service.test.ts
│ │ │ ├── ui.service.test.ts
│ │ │ └── update.service.test.ts
│ │ └── ui/
│ │ └── results.ui.test.ts
│ ├── core/
│ │ ├── npkill.test.ts
│ │ └── services/
│ │ ├── config.service.test.ts
│ │ ├── files/
│ │ │ ├── files.service.test.ts
│ │ │ ├── files.worker.service.test.ts
│ │ │ └── files.worker.test.ts
│ │ └── logger.service.test.ts
│ ├── index.test.ts
│ ├── main.test.ts
│ └── utils/
│ └── utils.test.ts
├── tsconfig.json
└── tslint.json
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/CODE_OF_CONDUCT.es.md
================================================
# Código de Conducta
## Nuestro Compromiso
En el interés de fomentar un entorno abierto y acogedor, nosotros como colaboradores
y mantenedores nos comprometemos a hacer que la participación en nuestro proyecto y
comunidad sea una experiencia libre de acoso para todos, independientemente de edad,
tamaño corporal, discapacidad, etnia, características sexuales, identidad de género
y expresión, nivel de experiencia, educación, estatus socioeconómico, nacionalidad,
apariencia personal, raza, religión o identidad y orientación sexual.
## Nuestras Normas
Ejemplos de comportamiento que contribuyen a crear un entorno positivo son:
* Utilizar lenguaje inclusivo
* Ser respetuoso con experiencias y puntos de vista distintos al nuestro
* Aceptar críticas contructivas de forma cortés
* Centrarnos en lo que sea mejor para la comunidad
* Mostrar empatía hacia otros miembros de la comunidad
Ejemplos de comportamiento inaceptable son:
* Uso de lenguaje sexualizado o imágenes sexuales, así como avances sexuales
indeseados
* Trolling, comentarios insultantes/despectivos, y ataques personales o políticos
* Acoso público o privado
* Publicar la información privada de terceros, como direcciones físicas o electrónicas,
sin permiso explícito.
* Cualquier conducta que, de forma razonable, se considere inapropiada en un ámbito
profesional.
## Nuestras Responsabilidades
Los mantenedores del proyecto son responsables de aclarar las normas de
comportamiento aceptable y se espera de ellos que tomen medidas apropiadas
en respuesta a cualquier instancia de comportamiento inaceptable.
Los mantenedores del proyecto tienen el derecho y la responsabilidad de eliminar,
editar o rechazar comentarios, commits, código, ediciones de wiki, issues y
cualquier otra contribución que incumpla este código de conducta, o de banear
temporal o peramenentemente a cualquier colaborador por cualquier comportamiento
que se considere inapropiado, amenazador, ofensivo o dañino.
## Ámbito
Este Código de Conducta se aplica tanto en el entorno del proyecto como en espacios
públicos donde un individuo representa al proyecto o a su comunidad. Ejemplos de
representar un proyecto o comunidad incluyen utilizar un e-mail oficial del proyecto,
publicaciones hechas vía una cuenta oficial en una red social, o actuar como un
representante oficial en cualquier evento online u offline. La representación de un
proyecto puede ser ampliada o aclarada por los mantenedores del proyecto.
## Aplicación
Instancias de comportamiento abusivo, acosador o inaceptable en cualquier otro sentido
pueden ser comunicadas al equipo del proyecto, a través de las direcciones de correo
electrónico nyablk97@gmail.com o juaniman.2000@gmail.com. Todas las quejas serán
revisadas e investigadas y resultarán en la respuesta que se considere necesaria y
apropiada según las circunstancias. El equipo del proyecto está obligado a mantener
la confidencialidad de cualquier persona que informe de un incidente.
Los mantenedores del proyecto que no sigan ni apliquen el Código de Conducta pueden
enfrentarse a repercusiones temporales o permanentes, determinadas por otros miembros
del equipo del proyecto.
## Atribución
Este Código de Conducta está adaptado del [Contributor Covenant][homepage], version 1.4,
disponible en https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
[homepage]: https://www.contributor-covenant.org
Para ver respuestas a preguntas comunes sobre este código de conducta, véase
https://www.contributor-covenant.org/faq
================================================
FILE: .github/CODE_OF_CONDUCT.md
================================================
# Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, sex characteristics, gender identity and expression,
level of experience, education, socio-economic status, nationality, personal
appearance, race, religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment
include:
- Using welcoming and inclusive language
- Being respectful of differing viewpoints and experiences
- Gracefully accepting constructive criticism
- Focusing on what is best for the community
- Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
- The use of sexualized language or imagery and unwelcome sexual attention or
advances
- Trolling, insulting/derogatory comments, and personal or political attacks
- Public or private harassment
- Publishing others' private information, such as a physical or electronic
address, without explicit permission
- Other conduct which could reasonably be considered inappropriate in a
professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community. Examples of
representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an appointed
representative at an online or offline event. Representation of a project may be
further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team at nyablk97@gmail.com or juaniman.2000@gmail.com. All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.
Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see
https://www.contributor-covenant.org/faq
================================================
FILE: .github/CONTRIBUTING.es.md
================================================
**_(Este doc está en proceso de desarrollo)_**
# Cómo contribuir a NPKILL 🎉
Sé que lo que voy a decir es lo típico, pero es realmente maravilloso que estés leyendo estas líneas. Quiere decir que estás interesad@ en ayudar a mejorar Npkill, _o quizá simplemente estés aquí por curiosidad `cof cof`_.
Sea por la razón que sea, eres bienvenid@. A continuación te explico las pautas recomendadas a la hora de contribuir.
---
# Consideraciones habituales
- Seguir este protocolo ayuda a evitar trabajar en vano. Sería una pena dedicar horas a un pull request y que tengamos que rechazarlo porque ya hay alguien trabajando en un issue similar.
- A no ser que sean modificaciones menores y rápidas, intenta informar a todo el mundo de que estás modificando algo. Para ello puedes abrir un issue, o consultar los [proyectos](https://github.com/voidcosmos/npkill/projects).
- Cambia únicamente las líneas que sean necesarias para llevar a cabo la modificación. Esto ayudará a evitar conflictos, y en el caso de que exista alguno, será más fácil de solucionar.
- Asegúrate de ejecutar `npm install`, ya que algunos paquetes de desarrollo existen para mantener la armonía. Prettier, por ejemplo, se asegura en cada commit de que los ficheros tienen la sangría correctamente, y Commitlint se asegura de que los mensajes de commit siguen la convención.
- Siempre que sea posible, añade tests, tests y... ¡Más tests! tests tests tests tests tests tests tests tests tests tests tests
# Nueva feature
1. Si quieres contribuir con una nueva feature, asegúrate de que no hay un issue anterior de otra persona trabajando en lo mismo.
2. Si no hay, abre un issue explicando lo que quieres incorporar, y los ficheros que, a priori, creas que tendrás que modificar.
3. Espera a que la comunidad se pronuncie, y a que algún miembro apruebe tu propuesta (decisión que se tendrá un cuenta por la comunidad).
¡Bien! ¡Luz verde para picar!
4. Haz un fork de este proyecto.
5. Crea una nueva rama siguiendo las convenciones recomendadas.
6. Escribe el código y crea commits de forma regular siguiendo la convención recomendada.
7. Crea un PULL REQUEST utilizando **master como rama base**.
Como título, utiliza uno igual o similar al que utilizaste en la creación del issue, y en la descripción, cualquier información que consideres relevante junto al enlace al issue y el mensaje "close". Ejemplo: close #numeroIssue
[más info](https://help.github.com/en/articles/closing-issues-using-keywords)
# Convenciones
## Ramas de git
Recomendamos utilizar la siguiente nomenclatura siempre que sea posible:
- feat/sort-results
- fix/lstat-crash
- docs/improve-readme
## Mensajes de git
Asegúrate de pensar bien el mensaje de cada commit.
Todos los commits deben utilizar una convención similar a la de `Angular`. [Aquí tienes todas las reglas](https://github.com/conventional-changelog/commitlint/tree/master/%40commitlint/config-conventional#type-enum)
- Utiliza el presente ("add feature", no "added feature")
- Utiliza el imperativo ("move cursor to", no "moves cursor to")
- Limita la primera línea a 72 caracteres o menos
- Referencia issues y pull request tanto como quieras tras la primera línea
_[Some points extracted from Atom doc](https://github.com/atom/atom/blob/master/CONTRIBUTING.md#git-commit-messages)_
## Código
Es importante aplicar los principios del código limpio.
Si utilizas `VS Code`, a continuación tienes algunos add-ons que recomendamos:
- TSLint: Te permite saber si estás incumpliendo algunas de las _reglas de código_ (no utilizar var, utilizar const siempre que sea posible, tipar siempre las variables etc.)
- CodeMetrics: Calcula la complejidad de los métodos, para asegurar que cada función hace únicamente 1 cosa. (verde es ok, amarillo es meh, rojo es oh god why)
Si utilizas otro IDE, probablemente haya add-ons parecidos disponibles.
================================================
FILE: .github/CONTRIBUTING.md
================================================
**_(this doc is under construction)_**
# How to contribute on NPKILL 🎉
I know that what I am going to say sounds like something typical, but I am sincerely glad that you are reading this, because that means that you are interested in helping to improve Npkill, _or you may simply be here out of curiosity `cof cof`_.
Anyway, you are sincerely welcome. I will try to explain the recommended guidelines to contribute.
---
# Common considerations
- Following this protocol helps to avoid working in vain. It would be a shame to dedicate hours to a pull request and have to reject it because there is already someone working on a similar issue.
-Unless they are minor and fast modifications, try to let everyone know that you are modifying something by opening an issue for example, or consulting the [projects](https://github.com/voidcosmos/npkill/projects)
- Change only the necessary lines for your modification. This will help to avoid conflicts, and in case of there being any, it will be easier to solve them.
- Make sure you to run `npm install`, because some development packages are meant to maintain harmony. Prettier, for example, makes sure that in each commit the files are well indented, and Commitlint makes sure that your messages follow the convention.
- Whenever possible, write tests, tests and more tests! tests tests tests tests tests tests tests tests tests tests tests
# New feature
1. If you want to contribute to a new feature, make sure that there isn't a previous issue of someone working on the same feature.
2. Then, open an issue explaining what you want to incorporate, and the files that you think you will need to modify a priori.
3. Wait for the community to give an opinion, and for some member to approve your proposal (a decision that will be taken into the community and future plans).
Yay! Green light to work!
4. Fork this project.
5. Create a new branch following the [recommended conventions]()
6. Write code and create commits regularly following the [recommended convention]()
7. Create a PULL REQUEST using **master as the base branch**.
As a title, use the same (or similar) one you used in the creation of the issue, and in the description, any information that you consider relevant next to the link of the issue and "close" text (example: close #issueNumber) [more info](https://help.github.com/en/articles/closing-issues-using-keywords)
# Conventions
## git branch
I recommend using the following nomenclature whenever possible:
- feat/sort-results
- fix/lstat-crash
- docs/improve-readme
## git messages
Be sure to take your time thinking about the message for each commit.
All commits must use a convention similar to `Angular`. [Here all the rules](https://github.com/conventional-changelog/commitlint/tree/master/%40commitlint/config-conventional#type-enum)
- Use the present tense ("add feature" not "added feature")
- Use the imperative mood ("move cursor to..." not "moves cursor to...")
- Limit the first line to 72 characters or less
- Reference issues and pull requests liberally after the first line
_[Some points extracted from Atom doc](https://github.com/atom/atom/blob/master/CONTRIBUTING.md#git-commit-messages)_
## code
It is important to apply the principles of clean code.
If you use `VS Code`, there are some add-ons that I recommend:
-TSLint: Lets you know if you are breaking any of the _coding rules_ (do not use var, use const if possible, if some type has not been defined etc)
- CodeMetrics: Calculates the complexity of the methods, to ensure that your functions do only 1 thing. (green is ok, yellow is meh, red is oh god why)
If you use a different IDE, there are probably similar add-ons available.
================================================
FILE: .github/FUNDING.yml
================================================
# These are supported funding model platforms
open_collective: npkill
custom: ['ethereum/0x7668e86c8bdb52034606db5aa0d2d4d73a0d4259']
================================================
FILE: .github/ISSUE_TEMPLATE/bug-report.md
================================================
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Command '....'
3. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
- OS: [e.g. Window]
- Version [ npkill -v ]
**Additional context**
Add any other context about the problem here.
================================================
FILE: .github/ISSUE_TEMPLATE/feature_request.md
================================================
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: ''
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.
================================================
FILE: .github/workflows/codeql-analysis.yml
================================================
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: 'CodeQL'
on:
schedule:
- cron: '25 8 * * 1'
workflow_dispatch:
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language: ['typescript']
# Learn more about CodeQL language support at https://git.io/codeql-language-support
steps:
- name: Checkout repository
uses: actions/checkout@v2
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# queries: ./path/to/local/query, your-org/your-repo/queries@main
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v2
# ℹ️ Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
# and modify them (or add more) to build your code if your project
# uses a compiled language
#- run: |
# make bootstrap
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2
================================================
FILE: .github/workflows/nodejs.yml
================================================
# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
name: Node.js CI
on: [push, pull_request, pull_request_review]
jobs:
testing:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, windows-latest]
node-version: [18, 20, 22, 24]
include:
- os: macos-latest
node-version: 21
steps:
- uses: actions/checkout@v4
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- name: Install Dependencies
run: npm ci --ignore-scripts
- run: npm test
env:
CI: true
================================================
FILE: .gitignore
================================================
node_modules
lib
out.txt
# stryker temp files
.stryker-tmp
stryker.log
reports
coverage
stuff
test-files
docs/private
.npkillrc
================================================
FILE: .husky/commit-msg
================================================
npx --no -- commitlint --edit
================================================
FILE: .husky/pre-commit
================================================
npx lint-staged
================================================
FILE: .node-version
================================================
20.12.0
================================================
FILE: .npmignore
================================================
src
tsconfig.json
tslint.json
.prettierrc
================================================
FILE: .prettierrc
================================================
{
"trailingComma": "all",
"tabWidth": 2,
"semi": true,
"singleQuote": true
}
================================================
FILE: .vscode/launch.json
================================================
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Launch Program",
"skipFiles": ["<node_internals>/**"],
"program": "${workspaceFolder}/lib/index.js"
}
]
}
================================================
FILE: .vscode/settings.json
================================================
{
"editor.formatOnSave": true,
"debug.javascript.autoAttachFilter": "onlyWithFlag"
}
================================================
FILE: API.md
================================================
# NPKill API
This document does not include all project documentation at this stage. It brings together the basic concepts.
For more details see the project interfaces.
- [NPKill API](#npkill-api)
- [Interface: `Npkill`](#interface-npkill)
- [`startScan$(rootPath, options?)`](#startscanrootpath-options)
- [`stopScan()`](#stopscan)
- [`getSize$(path, options?)`](#getsizepath-options)
- [`getNewestFile$(path)`](#getnewestfilepath)
- [`delete$(path, options?)`](#deletepath-options)
- [`getLogs$()`](#getlogs)
- [`isValidRootFolder(path)`](#isvalidrootfolderpath)
- [`getVersion()`](#getversion)
- [Interfaces & Types](#interfaces-types)
- [`ScanOptions`](#scanoptions)
- [`ScanFoundFolder`](#scanfoundfolder)
- [`RiskAnalysis`](#riskanalysis)
- [`GetSizeOptions`](#getsizeoptions)
- [`GetSizeResult`](#getsizeresult)
- [`GetNewestFileResult`](#getnewestfileresult)
- [`DeleteOptions`](#deleteoptions)
- [Usage Example](#usage-example)
---
## Interface: `Npkill`
The core of the system is the `NpkillInterface`. It offers methods to:
- Scan folders recursively.
- Get metadata about folders (size, last modified).
- Perform safe deletions.
- Stream logs and validate folders.
### `startScan$(rootPath, options?)`
Starts a recursive scan from a given root folder.
- **Parameters**:
- `rootPath`: `string` — Folder to start scanning from.
- `options`: [`ScanOptions`](#scanoptions) — Optional scan configuration.
- **Returns**: `Observable<ScanFoundFolder>`
- **Description**: Emits each matching folder as it's found.
---
### `stopScan()`
Stops any ongoing scan and releases resources.
---
### `getSize$(path, options?)`
Returns the total size of a directory.
- **Parameters**:
- `path`: `string` — Path to folder.
- `options`: [`GetSizeOptions`](#getsizeoptions)
- **Returns**: `Observable<GetSizeResult>`
---
### `getNewestFile$(path)`
Gets the most recently modified file inside a directory (recursively).
- **Parameters**:
- `path`: `string`
- **Returns**: `Observable<GetNewestFileResult | null>`
---
### `delete$(path, options?)`
Deletes a folder, optionally as a dry-run. Only allowed if the folder is within the `target` of the initial scan.
- **Parameters**:
- `path`: `string`
- `options`: [`DeleteOptions`](#deleteoptions)
- **Returns**: `Observable<DeleteResult>`
- **Throws**: If the path is outside the original target.
---
### `getLogs$()`
Streams internal log entries.
- **Returns**: `Observable<LogEntry[]>`
---
### `isValidRootFolder(path)`
Validates whether a folder is suitable for scanning.
- **Parameters**:
- `path`: `string`
- **Returns**: [`IsValidRootFolderResult`](#isvalidrootfolderresult)
---
### `getVersion()`
Returns the current version of npkill from `package.json`.
- **Returns**: `string`
---
## Interfaces & Types
---
### `ScanOptions`
```ts
interface ScanOptions {
targets: string[];
exclude?: string[];
sortBy?: 'path' | 'size' | 'age';
performRiskAnalysis?: boolean; // Default: true
}
```
---
### `ScanFoundFolder`
```ts
interface ScanFoundFolder {
path: string;
riskAnalysis?: RiskAnalysis;
}
```
---
### `RiskAnalysis`
Determines whether a result is safe to delete. That is, if it is likely to belong to some application and deleting it could break it.
```ts
interface RiskAnalysis {
isSensitive: boolean;
reason?: string;
}
```
---
### `GetSizeOptions`
```ts
interface GetSizeOptions {
unit?: 'bytes'; // Default: 'bytes'
}
```
---
### `GetSizeResult`
```ts
interface GetSizeResult {
size: number;
unit: 'bytes';
}
```
---
### `GetNewestFileResult`
```ts
interface GetNewestFileResult {
path: string;
name: string;
timestamp: number;
}
```
---
### `DeleteOptions`
```ts
interface DeleteOptions {
dryRun?: boolean;
}
```
---
## Usage Example
This is a minimal example where:
1. it will start a search for `.nx` folders.
2. Get the most recent file
3. Get the total size of the directory
```ts
import { Npkill } from 'npkill';
import { mergeMap, filter, map } from 'rxjs';
const npkill = new Npkill();
let files: {
path: string;
size: number;
newestFile: string;
}[] = [];
npkill
.startScan$('/home/user/projects/', { target: '.nx' })
.pipe(
// Step 1: For each scan result, get the newest file
mergeMap((scanResult) =>
npkill.getNewestFile$(scanResult.path).pipe(
// Step 2: If no newest file, skip this result
filter((newestFile) => newestFile !== null),
// Step 3: Combine scanResult and newestFile
map((newestFile) => ({
path: scanResult.path,
newestFile: newestFile.path,
})),
),
),
// Step 4: For each result, get the folder size
mergeMap((result) =>
npkill.getSize$(result.path).pipe(
map(({ size }) => ({
...result,
size,
})),
),
),
)
.subscribe({
next: (result) => {
files.push(result);
},
complete: () => {
console.log('✅ Scan complete. Found folders:', files.length);
console.table(files);
console.log(JSON.stringify(files));
},
});
```
Output:
```bash
✅ Scan complete. Found folders: 3
┌─────────┬───────────────────────────────────────────┬──────────────────────────────────────────────────────────────────────────┬─────────┐
│ (index) │ path │ newestFile │ size │
├─────────┼───────────────────────────────────────────┼──────────────────────────────────────────────────────────────────────────┼─────────┤
│ 0 │ '/home/user/projects/hello-world/.nx' │ '/home/user/projects/hello-world/.nx/cache/18.3.4-nx.linux-x64-gnu.node' │ 9388032 │
│ 1 │ '/home/user/projects/another-project/.nx' │ '/home/user/projects/another-project/.nx/workspace-data/d/daemon.log' │ 3182592 │
│ 2 │ '/home/user/projects/ARCHIVED/demo/.nx' │ '/home/user/projects/ARCHIVED/demo/.nx/cache/d/daemon.log' │ 2375680 │
└─────────┴───────────────────────────────────────────┴──────────────────────────────────────────────────────────────────────────┴─────────┘
[
{
"path": "/home/user/projects/hello-world/.nx",
"newestFile": "/home/user/projects/hello-world/.nx/cache/18.3.4-nx.linux-x64-gnu.node",
"size": 9388032
},
{
"path": "/home/user/projects/another-project/.nx",
"newestFile": "/home/user/projects/another-project/.nx/workspace-data/d/daemon.log",
"size": 3182592
},
........
]
```
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2025 Estefanía García Gallardo and Juan Torres Gómez
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.es.md
================================================
<p align="center">
<img src="https://npkill.js.org/img/npkill-text-outlined.svg" width="320" alt="npkill logo" />
<img src="https://npkill.js.org/img/npkill-scope-mono.svg" width="50" alt="npkill logo scope" />
</p>
<p align="center">
<img alt="npm" src="https://img.shields.io/npm/dy/npkill.svg">
<a href="#donations"><img src="https://img.shields.io/badge/donate-<3-red" alt="Donations Badge"/></a>
<img alt="npm version" src="https://img.shields.io/npm/v/npkill.svg">
<img alt="NPM" src="https://img.shields.io/npm/l/npkill.svg">
</p>
### Encuentra y **destruye** directorios <font color="red">**node_modules**</font> viejos y pesados :sparkles:
<p align="center">
<img src="/docs/npkill-demo-0.10.0.gif" alt="npkill demo GIF" />
</p>
Esta herramienta te permite listar cualquier directorio _node_modules_ que haya en tu sistema, además del espacio que ocupa. Entonces puedes seleccionar los que quieras borrar para liberar espacio. ¡Yay!
## i18n
Nos estamos esforzando por internacionalizar la documentación de Npkill. Aquí tienes una lista de las traducciones disponibles:
- [Español](./README.es.md)
- [Português](./README.pt.md)
## Table of Contents
- [Características](#features)
- [Instalación](#installation)
- [Uso](#usage)
- [Opciones](#options)
- [Ejemplos](#examples)
- [Configuración local](#setup-locally)
- [Roadmap](#roadmap)
- [Bugs conocidos](#known-bugs)
- [Cómo contribuir](#contributing)
- [Invítanos a un café](#donations)
- [Licencia](#license)
<a name="features"></a>
# :heavy_check_mark: Características
- **Libera espacio:** Elimina tus directorios _node_modules_ viejos y polvorientos que le roban espacio a tu máquina.
- **Último uso del Workspace**: Comprueba cuándo ha sido la última vez que has modificado un fichero en el workspace (indicado en la columna **last_mod**).
- **Rapidez:** NPKILL está escrito en TypeScript, pero las búsquedas se llevan a cabo a bajo nivel, lo que supone una mejora considerable del rendimiento.
- **Fácil de utilizar:** Despídete de comandos largos y difíciles. Utilizar Npkill es tan sencillo como leer la lista de tus node_modules, y pulsar la tecla Del para eliminarlos. ¿Podría ser más fácil? ;)
- **Minificado:** Apenas tiene dependencias.
<a name="installation"></a>
# :cloud: Instalación
¡Lo mejor es que no tienes que instalar Npkill para utilizarlo!
Simplemente utiliza el siguiente comando:
```bash
$ npx npkill
```
O, si por alguna razón te apetece instalarlo:
```bash
$ npm i -g npkill
# Los usuarios de Unix quizá tengan que ejecutar el comando con sudo. Ve con cuidado
```
> NPKILL no tiene soporte para node<v14. Si esto te afecta puedes utilizar `npkill@0.8.3`
<a name="usage"></a>
# :clipboard: Uso
```bash
$ npx npkill
# o solo npkill si está instalado de forma global
```
Por defecto, Npkill comenzará la búsqueda de node_modules comenzando en la ruta donde se ejecute el comando `npkill`.
Muévete por los distintos directorios listados con <kbd>↓</kbd> <kbd>↑</kbd>, y utiliza <kbd>Space</kbd> para borrar el directorio seleccionado.
También puedes usar <kbd>j</kbd> y <kbd>k</kbd> para moverte por los resultados.
Puedes abrir el directorio donde se aloja el resultado seleccionado pulsando <kbd>o</kbd>.
Para salir de Npkill, utiliza <kbd>Q</kbd>, o si te sientes valiente, <kbd>Ctrl</kbd> + <kbd>c</kbd>.
**¡Importante!** Algunas aplicaciones que están instaladas en el sistema necesitan su directorio node_modules para funcionar, y borrarlo puede romperlas. NPKILL te mostrará un :warning: para que sepas que tienes que tener cuidado.
<a name="options"></a>
## Opciones
| ARGUMENTO | DESCRIPCIÓN |
| -------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| -c, --bg-color | Cambia el color de selección de la fila. _(Colores disponibles: **azul**, cyan, magenta, blanco, rojo y amarillo)_ |
| -d, --directory | Permite seleccionar el directorio desde el que comienza la búsqueda. Por defecto, se empieza en . |
| -D, --delete-all | Borra automáticamente todos los node_modules que se encuentren. Recomendable utilizar junto a `-x` |
| -e, --hide-errors | Esconde los errores en el caso de que ocurra alguno |
| -E, --exclude | Excluye directorios de la búsqueda (la lista de directorios debe estar entre comillas dobles "", cada directorio separado por ',' Ejemplo: "ignore1, ignore2") |
| -f, --full | Comienza la búsqueda en el home del usuario (ejemplo: "/home/user" en Linux) |
| -gb | Muestra el tamaño en Gigabytes en lugar de en Megabytes. |
| -h, --help, ? | Muestra esta página de ayuda y finaliza |
| -nu, --no-check-update | No comprobar si hay actualizaciones al iniciar la aplicación |
| -s, --sort | Ordena los resultados por: `size`, `path` or `last-mod` |
| -t, --target | Especifica el nombre del directorio que se buscará (por defecto es node_modules) |
| -x, --exclude-hidden-directories | Excluye directorios ocultos (directorios "dot") de la búsqueda |
| --dry-run | No borra nada (simula un tiempo de borrado aleatorio) |
| -v, --version | Muestra la versión de Npkill |
**Precaución:** _Algunos comandos pueden cambiar en versiones futuras_
<a name="examples"></a>
## Ejemplo
- Busca y encuentra los directorios **node_modules** en un directorio _projects_ :
```bash
npkill -d ~/projects
# otra alternativa:
cd ~/projects
npkill
```
- Lista los directorios llamados "dist" y muestra los errores que ocurran:
```bash
npkill --target dist -e
```
- Muestra el cursor de color magenta... ¡Porque me gusta el magenta!
```bash
npkill --bg-color magenta
```
- Lista los directorios **vendor** en un directorio _projects_, ordenados por tamaño y mostrando el tamaño en gb:
```bash
npkill -d '~/more projects' -gb --sort size --target vendor
```
- Lista los **node_modules** en el directorio _projects_, excluyendo los que están en los directorios _progress_ e _ignore-this_:
```bash
npkill -d 'projects' --exclude "progress, ignore-this"
```
- Borra automáticamente todos los **node_modules** que se encuentren en el directorio _backups_:
```bash
npkill -d ~/backups/ --delete-all
```
<a name="setup-locally"></a>
# :pager: Configuración local
```bash
# -- Primero, clona el repositorio
git clone https://github.com/voidcosmos/npkill.git
# -- Navega al dir
cd npkill
# -- Instala las dependencias
npm install
# -- ¡Y ejecuta!
npm run start
# -- Si quieres ejecutar con algún parámetro, hay que añadir "--", tal y como se muestra a continuación:
npm run start -- -f -e
```
<a name="roadmap"></a>
# :crystal_ball: Roadmap
- [x] Lanzar la versión 0.1.0 !
- [x] Mejorar el código
- [x] Mejorar el rendimiento
- [ ] ¡Mejorar el rendimiento aún más!
- [x] Ordenar los resultados por tamaño y ruta
- [x] Permitir la búsqueda de otro tipo de directorios (targets)
- [ ] Reducir las dependencies para ser un módulo más minimalista
- [ ] Permitir el filtrado por directorios que no se hayan utilizado en un periodo de tiempo determinado
- [ ] Crear una opción para mostrar los directorios en formato árbol
- [x] Añadir menús
- [x] Añadir un servicio de logs
- [ ] Limpieza periódica y automática (?)
<a name="known-bugs"></a>
# :bug: Bugs conocidos :bug:
- A veces, el CLI se bloquea mientras un directorio se está borrando.
- La ordenación, especialmente por rutas, puede ralentizar la terminal cuando haya muchos resultados al mismo tiempo.
- A veces, los cálculos de tamaño son mayores de lo que deberían ser.
- (RESUELTO) Problemas de rendimiento al hacer la búsqueda desde directorios de alto nivel (como / en Linux).
- (RESUELTO) A veces el texto se colapsa al actualizar el CLI.
- (RESUELTO) Analizar el tamaño de los directorios tarda más de lo que debería.
> Si encuentras algún bug, no dudes en abrir un issue :)
<a name="contributing"></a>
# :revolving_hearts: Cómo contribuir
Si quieres contribuir, échale un vistazo al [CONTRIBUTING.md](.github/CONTRIBUTING.es.md)
<a name="donations"></a>
# :coffee: Invítanos a un café
<img align="right" width="300" src="https://npkill.js.org/img/cat-donation-cup.png">
Hemos desarrollado Npkill en nuestro tiempo libre, porque nos apasiona la programación.
El día de mañana nos gustaría dedicarnos al open source completamente, pero antes, nos queda un largo camino por recorrer.
Seguiremos contribuyendo al open source por y para siempre, pero las donaciones son una de las muchas formas de apoyarnos.
¡Invítanos a un café! (O a un té para Nya, la única programadora a la que no le gusta el café).
<span class="badge-opencollective"><a href="https://opencollective.com/npkill/contribute" title="Dona a este proyecto utilizando Open Collective"><img src="https://img.shields.io/badge/open%20collective-donate-green.svg" alt="Botón de donar con Open Collective" /></a></span>
### ¡¡Mil gracias!!
## Muchísimas gracias a todos los que nos han apoyado :heart:
<a href="https://opencollective.com/npkill#backers" target="_blank"><img width="535" src="https://opencollective.com/npkill/tiers/backer.svg?width=535"></a>
---
### Alternativa cripto
- btc: 1ML2DihUoFTqhoQnrWy4WLxKbVYkUXpMAX
- bch: 1HVpaicQL5jWKkbChgPf6cvkH8nyktVnVk
- eth: 0x7668e86c8bdb52034606db5aa0d2d4d73a0d4259
<a name="license"></a>
# :scroll: Licencia
MIT © [Nya García Gallardo](https://github.com/NyaGarcia) y [Juan Torres Gómez](https://github.com/zaldih)
:cat::baby_chick:
---
================================================
FILE: README.id.md
================================================
<p align="center">
<img src="https://npkill.js.org/img/npkill-text-outlined.svg" width="320" alt="npkill logo" />
<img src="https://npkill.js.org/img/npkill-scope-mono.svg" width="50" alt="npkill logo scope" />
</p>
<p align="center">
<img alt="npm" src="https://img.shields.io/npm/dy/npkill.svg">
<a href="#donations"><img src="https://img.shields.io/badge/donate-<3-red" alt="Donations Badge"/></a>
<img alt="npm version" src="https://img.shields.io/npm/v/npkill.svg">
<img alt="NPM" src="https://img.shields.io/npm/l/npkill.svg">
</p>
### Mudah menemukan dan **menghapus** folder <font color="red">**node_modules**</font> yang lama dan berat :sparkles:
<p align="center">
<img src="/docs/npkill-demo-0.10.0.gif" alt="npkill demo GIF" />
</p>
Alat ini memungkinkan Anda untuk mencantumkan semua direktori _node_modules_ di sistem Anda, serta ruang yang mereka gunakan. Anda kemudian dapat memilih mana yang ingin Anda hapus untuk mengosongkan ruang penyimpanan. Yay!
## i18n
Kami berusaha untuk menerjemahkan dokumen Npkill ke berbagai bahasa. Berikut daftar terjemahan yang tersedia:
- [Español](./README.es.md)
- [Indonesian](./README.id.md)
- [Portugis](./README.pt.md)
- [Turki](./README.tr.md)
## Daftar Isi
- [Fitur](#features)
- [Instalasi](#installation)
- [Penggunaan](#usage)
- [Opsi](#options)
- [Contoh](#examples)
- [Pengaturan Lokal](#setup-locally)
- [Peta Jalan](#roadmap)
- [Bug yang Diketahui](#known-bugs)
- [Kontribusi](#contributing)
- [Buy us a coffee](#donations)
- [Lisensi](#license)
<a name="features"></a>
# :heavy_check_mark: Fitur
- **Bersihkan Ruang:** Hapus _node_modules_ lama yang tidak digunakan yang memenuhi mesin Anda.
- **Penggunaan Terakhir Workspace:** Cek kapan terakhir kali Anda mengubah file di workspace (ditunjukkan di kolom **last_mod**).
- **Sangat Cepat:** NPKILL ditulis dalam TypeScript, tetapi pencarian dilakukan di tingkat rendah, sehingga performanya sangat baik.
- **Mudah Digunakan:** Tidak perlu perintah panjang. Menggunakan npkill semudah membaca daftar _node_modules_ Anda, dan menekan tombol Del untuk menghapusnya. Bisa lebih mudah dari itu?
- **Ringkas:** Hampir tidak memiliki dependensi.
<a name="installation"></a>
# :cloud: Instalasi
Anda tidak perlu menginstal untuk menggunakannya! Cukup gunakan perintah berikut:
```bash
$ npx npkill
```
Atau jika Anda benar-benar ingin menginstalnya:
```bash
$ npm i -g npkill
# Pengguna Unix mungkin perlu menjalankan perintah dengan sudo. Gunakan dengan hati-hati
```
> NPKILL tidak mendukung node<v14. Jika ini memengaruhi Anda, gunakan `npkill@0.8.3`
<a name="usage"></a>
# :clipboard: Penggunaan
```bash
$ npx npkill
# atau cukup npkill jika telah diinstal secara global
```
Secara default, npkill akan memindai _node_modules_ mulai dari jalur tempat perintah `npkill` dijalankan.
Pindah di antara folder yang terdaftar menggunakan <kbd>↓</kbd> <kbd>↑</kbd>, dan gunakan <kbd>Space</kbd> atau <kbd>Del</kbd> untuk menghapus folder yang dipilih. Anda juga dapat menggunakan <kbd>j</kbd> dan <kbd>k</kbd> untuk bergerak di antara hasil.
Anda dapat membuka direktori tempat hasil yang dipilih berada dengan menekan <kbd>o</kbd>.
Untuk keluar, tekan <kbd>Q</kbd> atau <kbd>Ctrl</kbd> + <kbd>c</kbd> jika Anda pemberani.
**Penting!** Beberapa aplikasi yang diinstal di sistem membutuhkan direktori _node_modules_ untuk berfungsi, dan menghapusnya dapat menyebabkan kerusakan. NPKILL akan menandainya dengan :warning: agar berhati-hati.
<a name="options"></a>
## Opsi
| ARGUMEN | DESKRIPSI |
| -------------------------------- | ------------------------------------------------------------------------------------------------------------- |
| -c, --bg-color | Ubah warna sorotan baris. _(Tersedia: **blue**, cyan, magenta, white, red, dan yellow)_ |
| -d, --directory | Tetapkan direktori awal pencarian. Secara default, mulai dari . |
| -D, --delete-all | Secara otomatis hapus semua folder _node_modules_ yang ditemukan. Disarankan digunakan bersama `-x`. |
| -e, --hide-errors | Sembunyikan kesalahan (jika ada) |
| -E, --exclude | Kecualikan direktori dari pencarian. Daftar direktori harus dalam tanda kutip ganda "", dipisahkan dengan ',' |
| -f, --full | Mulai pencarian dari direktori home pengguna (contoh: "/home/user" di Linux) |
| -gb | Tampilkan folder dalam Gigabyte daripada Megabyte. |
| -h, --help, ? | Tampilkan halaman bantuan ini dan keluar |
| -nu, --no-check-update | Jangan memeriksa pembaruan saat startup |
| -s, --sort | Urutkan hasil berdasarkan: `size`, `path`, atau `last-mod` |
| -t, --target | Tentukan nama direktori yang ingin Anda cari (default: node_modules) |
| -x, --exclude-hidden-directories | Kecualikan direktori tersembunyi dari pencarian. |
| --dry-run | Tidak menghapus apa pun (hanya simulasi dengan delay acak). |
| -v, --version | Tampilkan versi npkill |
**Peringatan:** _Di versi mendatang, beberapa perintah mungkin berubah._
<a name="examples"></a>
## Contoh
- Cari direktori **node_modules** di direktori _projects_ Anda:
```bash
npkill -d ~/projects
# alternatif lain:
cd ~/projects
npkill
```
- Daftar direktori bernama "dist" dan tampilkan kesalahan jika ada:
```bash
npkill --target dist -e
```
- Tampilkan kursor warna magenta... karena saya suka magenta!
```bash
npkill --color magenta
```
- Daftar direktori **vendor** di _projects_, urutkan berdasarkan ukuran, dan tampilkan ukuran dalam GB:
```bash
npkill -d '~/more projects' -gb --sort size --target vendor
```
- Secara otomatis hapus semua _node_modules_ di folder cadangan Anda:
```bash
npkill -d ~/backups/ --delete-all
```
<a name="setup-locally"></a>
# :pager: Pengaturan Lokal
```bash
# -- Pertama, kloning repositori
git clone https://github.com/voidcosmos/npkill.git
# -- Masuk ke direktori
cd npkill
# -- Instal dependensi
npm install
# -- Dan jalankan!
npm run start
# -- Jika ingin menjalankannya dengan parameter, tambahkan "--" seperti contoh berikut:
npm run start -- -f -e
```
<a name="roadmap"></a>
# :crystal_ball: Peta Jalan
- [x] Rilis versi 0.1.0!
- [x] Tingkatkan kode
- [x] Tingkatkan performa
- [ ] Tingkatkan performa lebih lanjut!
- [x] Urutkan hasil berdasarkan ukuran dan jalur
- [x] Izinkan pencarian untuk jenis direktori (target) lainnya
- [ ] Kurangi dependensi agar minimalis
- [ ] Filter berdasarkan waktu terakhir penggunaan
- [ ] Tampilkan direktori dalam format tree
- [x] Tambahkan beberapa menu
- [x] Tambahkan log
- [ ] Pembersihan otomatis berkala (?)
<a name="known-bugs"></a>
# :bug: Bug yang Diketahui :bug:
- CLI terkadang berhenti saat menghapus folder.
- Beberapa terminal tanpa TTY (seperti Git Bash di Windows) tidak bekerja.
- Mengurutkan berdasarkan jalur dapat memperlambat terminal dengan banyak hasil.
- Perhitungan ukuran kadang lebih besar dari seharusnya.
- (TERPECAHKAN) Masalah performa pada direktori tingkat tinggi (seperti / di Linux).
- (TERPECAHKAN) Teks terkadang kacau saat CLI diperbarui.
- (TERPECAHKAN) Analisis ukuran direktori memakan waktu lebih lama dari seharusnya.
> Jika menemukan bug, jangan ragu untuk membuka issue. :)
<a name="contributing"></a>
# :revolving_hearts: Kontribusi
Jika ingin berkontribusi, cek [CONTRIBUTING.md](.github/CONTRIBUTING.md).
<a name="donations"></a>
# :coffee: Buy us a coffee
<img align="right" width="300" src="https://npkill.js.org/img/cat-donation-cup.png">
Kami mengembangkan npkill di waktu luang karena kami mencintai pemrograman.
Kami akan terus mengerjakan ini, tetapi donasi adalah salah satu cara mendukung apa yang kami lakukan.
<span class="badge-opencollective"><a href="https://opencollective.com/npkill/contribute" title="Donate to this project using Open Collective"><img src="https://img.shields.io/badge/open%20collective-donate-green.svg" alt="Open Collective donate button" /></a></span>
### Terima Kasih!!
## Terima kasih banyak kepada pendukung kami :heart:
<a href="https://opencollective.com/npkill#backers" target="_blank"><img width="535" src="https://opencollective.com/npkill/tiers/backer.svg?width=535"></a>
---
### Alternatif Crypto
- btc: 1ML2DihUoFTqhoQnrWy4WLxKbVYkUXpMAX
- bch: 1HVpaicQL5jWKkbChgPf6cvkH8nyktVnVk
- eth: 0x7668e86c8bdb52034606db5aa0d2d4d73a0d4259
<a name="license"></a>
# :scroll: Lisensi
MIT © [Nya García Gallardo](https://github.com/NyaGarcia) dan [Juan Torres Gómez](https://github.com/zaldih)
:cat::baby_chick:
---
================================================
FILE: README.md
================================================
<p align="center">
<img src="./docs/npkill-text-clean.svg" width="380" alt="npkill logo" />
</p>
<p align="center">
<img alt="npm" src="https://img.shields.io/npm/dy/npkill.svg">
<a href="#donations"><img src="https://img.shields.io/badge/donate-<3-red" alt="Donations Badge"/></a>
<img alt="npm version" src="https://img.shields.io/npm/v/npkill.svg">
<img alt="NPM" src="https://img.shields.io/npm/l/npkill.svg">
</p>
### Easily find and **remove** old and heavy <font color="red">**node_modules**</font> folders :sparkles:
<p align="center">
<img src="/docs/npkill-demo-0.10.0.gif" alt="npkill demo GIF" />
</p>
This tool allows you to list any _node_modules_ directories in your system, as well as the space they take up. You can then select which ones you want to erase to free up space. Yay!
## i18n
We're making an effort to internationalize the Npkill docs. Here's a list of the available translations:
- [Español](./README.es.md)
- [Indonesian](./README.id.md)
- [Português](./README.pt.md)
- [Turkish](./README.tr.md)
## Table of Contents
- [Features](#features)
- [Installation](#installation)
- [Usage](#usage)
- [Multi-Select Mode](#multi-select-mode)
- [Options](#options)
- [Examples](#examples)
- [JSON Output](#json-output)
- [Set Up Locally](#setup-locally)
- [API](#API)
- [Roadmap](#roadmap)
- [Known bugs](#known-bugs)
- [Contributing](#contributing)
- [Buy us a coffee](#donations)
- [License](#license)
<a name="features"></a>
# :heavy_check_mark: Features
- **Clear space:** Get rid of old and dusty _node_modules_ cluttering up your machine.
- **Last Workspace Usage**: Check when was the last time you modified a file in the workspace (indicated in the **last_mod** column).
- **Very fast:** NPKILL is written in TypeScript, but searches are performed at a low level, improving performance greatly.
- **Easy to use:** Say goodbye to lengthy commands. Using npkill is as simple as reading a list of your node_modules, and pressing Del to get rid of them. Could it be any easier? ;)
- **Minified:** It barely has any dependencies.
<a name="installation"></a>
# :cloud: Installation
You don't really need to install it to use it!
Simply use the following command:
```bash
$ npx npkill
```
Or if for some reason you really want to install it:
```bash
$ npm i -g npkill
# Unix users may need to run the command with sudo. Go carefully
```
> NPKILL does not support node<v14. If this affects you you can use `npkill@0.8.3`
<a name="usage"></a>
# :clipboard: Usage
```bash
$ npx npkill
# or just npkill if installed globally
```
By default, npkill will scan for node_modules starting at the path where `npkill` command is executed.
Move between the listed folders with <kbd>↓</kbd> <kbd>↑</kbd>, and use <kbd>Space</kbd> or <kbd>Del</kbd> to delete the selected folder.
You can also use <kbd>j</kbd> and <kbd>k</kbd> to move between the results.
You can open the directory where the selected result is placed by pressing <kbd>o</kbd>.
To exit, <kbd>Q</kbd> or <kbd>Ctrl</kbd> + <kbd>c</kbd> if you're brave.
**Important!** Some applications installed on the system need their node_modules directory to work and deleting them may break them. NPKILL will highlight them by displaying a :warning: to be careful.
## Search Mode
Search mode allows you to filter results. This can be particularly useful for limiting the view to a specific route or ensuring that only those results that meet the specified condition are “selected all.”
For example, you can use this expression to limit the results to those that are in the `work` directory and that include `data` somewhere in the path: `/work/.*/data`.
Press <kbd>/</kbd> to enter search mode. You can type a regex pattern to filter results.
Press <kbd>Enter</kbd> to confirm the search and navigate the filtered results, or <kbd>Esc</kbd> to clear and exit.
To exit from this mode, leave empty.
## Multi-Select Mode
This mode allows you to select and delete multiple folders at once, making it more efficient when cleaning up many directories.
### Entering Multi-Select Mode
Press <kbd>T</kbd> to toggle multi-select mode. When active, you'll see a selection counter and additional instructions at the top of the results.
### Controls
- **<kbd>Space</kbd>**: Toggle selection of the current folder.
- **<kbd>V</kbd>**: Start/end range selection mode.
- **<kbd>A</kbd>**: Toggle select/unselect all folders.
- **<kbd>Enter</kbd>**: Delete all selected folders.
- **<kbd>T</kbd>**: Unselect all and back to normal mode.
### Range Selection
After pressing <kbd>V</kbd> to enter range selection mode:
- Move the cursor with arrow keys, <kbd>j</kbd>/<kbd>k</kbd>, <kbd>Home</kbd>/<kbd>End</kbd>, or page up/down
- All folders between the starting position and current cursor position will be selected/deselected
- Press <kbd>V</kbd> again to exit range selection mode
<a name="options"></a>
## Options
| ARGUMENT | DESCRIPTION |
| ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| -p, --profiles | Allows you to select the [profile](./docs/profiles.md) (set of targets) to use. If no option is specified, the available ones will be listed _(**node** by default)_. |
| --config | Path to a custom .npkillrc configuration file. By default, npkill looks first for `./.npkillrc` and then for `~/.npkillrc`. |
| -d, --directory | Set the directory from which to begin searching. By default, starting-point is . |
| -D, --delete-all | Automatically delete all folders that are found. Suggested to be used together with `-x`. |
| -e, --hide-errors | Hide errors if any |
| -E, --exclude | Exclude directories from search (directory list must be inside double quotes "", each directory separated by ',' ) Example: "ignore1, ignore2" |
| -f, --full | Start searching from the home of the user (example: "/home/user" in linux) |
| --size-unit | Set the unit for displaying folder sizes. _(Available: **auto**, mb, gb)_. With auto, sizes < 1024MB are shown in MB (rounded), larger sizes in GB (with decimals). |
| -h, --help, ? | Show help page |
| -nu, --no-check-update | Don't check for updates on startup |
| -s, --sort | Sort results by: `size`, `path` or `age` |
| -t, --targets | Disable profiles feature and specify the name of the directories you want to search for. You can define multiple targets separating with comma. Ej. `-t node_modules,.cache`. |
| -x, --exclude-sensitive | Exclude sensitive directories. |
| -y | Avoid displaying a warning when executing --delete-all. |
| --dry-run | It does not delete anything (will simulate it with a random delay). |
| --json | Output results in JSON format at the end of the scan. Useful for automation and scripting. |
| --json-stream | Output results in streaming JSON format (one JSON object per line as results are found). Useful for real-time processing. |
| -v, --version | Show npkill version |
<a name="examples"></a>
## Examples
- Search **node_modules** directories in your _projects_ directory:
```bash
npkill -d ~/projects
# other alternative:
cd ~/projects
npkill
```
- List **node_modules** in your _projects_ directory, excluding the ones in _progress_ and _ignore-this_ directories:
```bash
npkill -d 'projects' --exclude "progress, ignore-this"
```
- Automatically delete all node_modules that have sneaked into your backups:
```bash
npkill -d ~/backups/ --delete-all
```
- Get results in JSON format for automation or further processing:
```bash
npkill --json > results.json
```
- Stream results in real-time as JSON (useful for monitoring or piping to other tools):
```bash
npkill --json-stream | jq '.'
```
- Save only successful results to a file, ignoring errors:
```bash
npkill --json-stream 2>/dev/null | jq -s '.' > clean-results.json
```
<a name="json-output"></a>
## JSON Output
Npkill supports JSON output formats for automation and integration with other tools:
- **`--json`**: Output all results as a single JSON object at the end of the scan
- **`--json-stream`**: Output each result as a separate JSON object in real-time
For detailed documentation, examples, and TypeScript interfaces, see [JSON Output Documentation](./docs/json-output.md).
**Quick Examples:**
```bash
# Get all results as JSON
npkill --json > results.json
# Process results in real-time
npkill --json-stream | jq '.result.path'
# Find directories larger than 100MB
npkill --json | jq '.results[] | select(.size > 104857600)'
```
<a name="setup-locally"></a>
# :pager: Set Up Locally
```bash
# -- First, clone the repository
git clone https://github.com/voidcosmos/npkill.git
# -- Navigate to the dir
cd npkill
# -- Install dependencies
npm install
# -- And run!
npm run start
# -- If you want to run it with some parameter, you will have to add "--" as in the following example:
npm run start -- -f -e
```
<a name="API"></a>
# :bookmark_tabs: API
The api allows you to interact with npkill from node to create your own implementations in your scripts (automations, for example).
You can check the basic API [here](./API.md) or on the web (comming soon).
<a name="roadmap"></a>
# :crystal_ball: Roadmap
- [x] Release 0.1.0 !
- [x] Improve code
- [x] Improve performance
- [ ] Improve performance even more!
- [x] Sort results by size and path
- [x] Allow the search for other types of directories (targets)
- [ ] Reduce dependencies to be a more minimalist module
- [ ] Allow to filter by directories that have not been used in a period of time
- [ ] Create option for displaying directories in tree format
- [x] Add some menus
- [x] Add log service
- [ ] Periodic and automatic cleaning (?)
<a name="known-bugs"></a>
# :bug: Known bugs :bug:
- Sometimes, CLI is blocked while folder is deleting.
- Sorting, especially by routes, can slow down the terminal when there are many results at the same time.
- Sometimes, size calculations are higher than they should be.
- (SOLVED) Performance issues when searching from high level directories (like / in linux).
- (SOLVED) Sometimes text collapses when updating the cli.
- (SOLVED) Analyzing the size of the directories takes longer than it should.
> If you find any bugs, don't hesitate and open an issue :)
<a name="contributing"></a>
# :revolving_hearts: Contributing
If you want to contribute check the [CONTRIBUTING.md](.github/CONTRIBUTING.md)
<a name="donations"></a>
# :coffee: Buy us a coffee
<img align="right" width="300" src="https://npkill.js.org/img/cat-donation-cup.png">
We have developed npkill in our free time, because we are passionate about the programming sector.
Tomorrow we would like to dedicate ourselves to this, but first, we have a long way to go.
We will continue to do things anyway, but donations are one of the many ways to support what we do.
<span class="badge-opencollective"><a href="https://opencollective.com/npkill/contribute" title="Donate to this project using Open Collective"><img src="https://img.shields.io/badge/open%20collective-donate-green.svg" alt="Open Collective donate button" /></a></span>
### Thanks!!
## A huge thank you to our backers :heart:
<a href="https://opencollective.com/npkill#backers" target="_blank"><img width="535" src="https://opencollective.com/npkill/tiers/backer.svg?width=535"></a>
---
### Crypto alternative
- btc: 1ML2DihUoFTqhoQnrWy4WLxKbVYkUXpMAX
- bch: 1HVpaicQL5jWKkbChgPf6cvkH8nyktVnVk
- eth: 0x7668e86c8bdb52034606db5aa0d2d4d73a0d4259
<a name="license"></a>
# :scroll: License
MIT © [Nya García Gallardo](https://github.com/NyaGarcia) and [Juan Torres Gómez](https://github.com/zaldih)
:cat::baby_chick:
---
================================================
FILE: README.pt.md
================================================
<p align="center">
<img src="https://npkill.js.org/img/npkill-text-outlined.svg" width="320" alt="npkill logo" />
<img src="https://npkill.js.org/img/npkill-scope-mono.svg" width="50" alt="npkill logo scope" />
</p>
<p align="center">
<img alt="npm" src="https://img.shields.io/npm/dy/npkill.svg">
<a href="#donations"><img src="https://img.shields.io/badge/donate-<3-red" alt="Donations Badge"/></a>
<img alt="npm version" src="https://img.shields.io/npm/v/npkill.svg">
<img alt="NPM" src="https://img.shields.io/npm/l/npkill.svg">
</p>
### Encontre e **remova** facilemente pastas <font color="red">**node_modules**</font> antigas e pesadas :sparkles:
<p align="center">
<img src="/docs/npkill-demo-0.10.0.gif" alt="npkill demo GIF" />
</p>
Esta ferramenta permite que você liste as pastas _node_modules_ em seu sistema, bem como o espaço que ocupam. Então você pode selecionar quais deles deseja apagar para liberar espaço. ¡Yay!
## i18n
Estamos fazendo esforço para internacionalizar a documentação do Npkill. Aqui está uma lista das traduções disponíveis:
- [Español](./README.es.md)
- [Português](./README.pt.md)
## Table of Contents
- [Funcionalidades](#features)
- [Instalação](#installation)
- [Utilização](#usage)
- [Opções](#options)
- [Exemplos](#examples)
- [Configurar localmente](#setup-locally)
- [Roteiro](#roadmap)
- [Problemas conhecidos](#known-bugs)
- [Contribuindo](#contributing)
- [Compre-nos um café](#donations)
- [Licença](#license)
<a name="features"></a>
# :heavy_check_mark: Funcionalidades
- **Liberar espaço:** Livre-se dos antigos e empoeirados node_modules que ocupam espaço em sua máquina.
- **Último Uso do Espaço de Trabalho**: Verifique quando foi a última vez que você modificou um arquivo no espaço de trabalho (indicado na coluna **última_modificação**).
- **Muito rápido:** O NPKILL é escrito em TypeScript, mas as pesquisas são realizadas em um nível baixo, melhorando muito o desempenho.
- **Fácil de usar:** Diga adeus aos comandos longos. Usar o npkill é tão simples quanto ler uma lista de seus node_modules e pressionar Delete para se livrar deles. Pode ser mais fácil do que isso? ;)
- **Minificado:** Ele mal possui dependências.
<a name="installation"></a>
# :cloud: Instalação
Você nem precisa instalá-lo para usar!
Basta usar o seguinte comando:
```bash
$ npx npkill
```
Ou, se por algum motivo você realmente deseja instalá-lo:
```bash
$ npm i -g npkill
# Usuários do Unix podem precisar executar o comando com sudo. Tome cuidado.
```
> O NPKILL não suporta versões node<v14. Se isso afeta você, use npkill@0.8.3.
<a name="usage"></a>
# :clipboard: Utilização
```bash
$ npx npkill
# ou apenas npkill se você instalou globalmente
```
Por padrão, o npkill fará a varredura em busca de node_modules a partir do local onde o comando npkill é executado.
Para mover entre as pastas listadas, utilize as teclas <kbd>↓</kbd> e <kbd>↑</kbd>, e use <kbd>Space</kbd> ou <kbd>Del</kbd> para excluir a pasta selecionada.
Você também pode usar <kbd>j</kbd> e <kbd>k</kbd> para se mover entre os resultados.
Para abrir o diretório onde o resultado selecionado está localizado, pressione <kbd>o</kbd>.
Para sair, use <kbd>Q</kbd> ou <kbd>Ctrl</kbd> + <kbd>c</kbd> se você estiver se sentindo corajoso.
**Importante!** Algumas aplicações instaladas no sistema precisam do diretório node_modules delas para funcionar, e excluí-los pode quebrá-las. O NPKILL irá destacá-los exibindo um :warning: para que você tenha cuidado.
<a name="options"></a>
## Opções
| Comando | Descrição |
| -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| -c, --bg-color | Troca a cor de destaque da linha. _(Disponível: **blue**, cyan, magenta, white, red e yellow)_ |
| -d, --directory | Defina o diretório a partir do qual iniciar a pesquisa. Por padrão, o ponto de partida é a raiz is . |
| -D, --delete-all | Exclui automaticamente todos os node_modules encontrados. Recomendado para usar junto com `-x` |
| -e, --hide-errors | Oculta erros |
| -E, --exclude | Excluir diretórios da pesquisa (a lista de diretórios deve estar entre aspas duplas "", com cada diretório separado por vírgula ','). Exemplo: "ignorar1, ignorar2" |
| -f, --full | Iniciar a pesquisa a partir do diretório pessoal do usuário (exemplo: "/home/user" no Linux) |
| -gb | Mostra as pastas em Gigabytes ao invés de Megabytes. |
| -h, --help, ? | Mostrar a página de ajuda e sair |
| -nu, --no-check-update | Não verificar atualizações na inicialização |
| -s, --sort | Ordenar resultados por: `size` (tamanho), `path`(localização) ou `last-mod`(última modificação) |
| -t, --target | Especifique o nome dos diretórios que deseja pesquisar (por padrão, é node_modules) |
| -x, --exclude-hidden-directories | Excluir diretórios ocultos ("diretórios com ponto") da pesquisa. |
| --dry-run | Não exclui nada (irá simular com um atraso aleatório). |
| -v, --version | Mostrar versão do npkill |
**Aviso:** _No futuro alguns comandos podem mudar_
<a name="examples"></a>
## Examples
- Busque pastas **node_modules** no seu diretório de projetos:
```bash
npkill -d ~/projetos
# alternativa:
cd ~/projetos
npkill
```
- Listar diretórios com o nome "dist" e mostrar erros, se houver algum:
```bash
npkill --target dist -e
```
- Exibe o cursor na cor magenta... porque eu gosto de magenta!
```bash
npkill --bg-color magenta
```
- Listar pastas **vendor** no seu diretório de _projetos_, ordenar por tamanho e mostrar o tamanho em GB:
```bash
npkill -d '~/more projetos' -gb --sort size --target vendor
```
- Listar **node_modules** no seu diretório de _projetos_, exceto nas pastas _progresso_ e _ignorar_:
```bash
npkill -d 'projetos' --exclude "progresso, ignorar"
```
- Exclua automaticamente todos os node_modules que tenham entrado em seus backups:
```bash
npkill -d ~/backups/ --delete-all
```
<a name="setup-locally"></a>
# :pager: Configurar localmente
```bash
# -- Primeiramente, clone o repositório
git clone https://github.com/voidcosmos/npkill.git
# -- Acesse a pasta
cd npkill
# -- Instale as dependências
npm install
# -- E rode!
npm run start
# -- Se você deseja executá-lo com algum parâmetro, você terá que adicionar "--" como no seguinte exemplo:
npm run start -- -f -e
```
<a name="roadmap"></a>
# :crystal_ball: Roteiro
- [x] Lançamento 0.1.0 !
- [x] Melhorias de código
- [x] Melhorias de performance
- [ ] Ainda mais melhorias de performance!
- [x] Ordenação de resultados por tamanho e localização
- [x] Permitir a pesquisa por outros tipos de diretórios (alvo)
- [ ] Reduzir as dependências para tornar o módulo mais minimalista
- [ ] Permitir filtrar por diretórios que não foram usados em um período de tempo
- [ ] Criar opção para mostrar as pastas em formato de árvore
- [x] Adicionar menus
- [x] Adicionar logs
- [ ] Limpeza automatizada periódica (?)
<a name="known-bugs"></a>
# :bug: Problemas conhecidos :bug:
- Às vezes, a CLI fica bloqueada enquanto a pasta está sendo excluída.
- Alguns terminais que não utilizam TTY (como o git bash no Windows) não funcionam.
- A ordenação, especialmente por rotas, pode deixar o terminal mais lento quando há muitos resultados ao mesmo tempo.
- Às vezes, os cálculos de tamanho são maiores do que deveriam ser.
- (RESOLVIDO) Problemas de desempenho ao pesquisar em diretórios de alto nível (como / no Linux).
- (RESOLVIDO) Às vezes, o texto se desfaz ao atualizar a interface de linha de comando (CLI).
- (RESOLVIDO) A análise do tamanho dos diretórios leva mais tempo do que deveria.
> Se você encontrar algum erro, não hesite em abrir uma solicitação (via issue) :)
<a name="contributing"></a>
# :revolving_hearts: Contribuindo
Se você quer contribuir confira o [CONTRIBUTING.md](.github/CONTRIBUTING.md)
<a name="donations"></a>
# :coffee: Compre-nos um café
<img align="right" width="300" src="https://npkill.js.org/img/cat-donation-cup.png">
Desenvolvemos o npkill em nosso tempo livre, porque somos apaixonados pelo setor de programação. Amanhã, gostaríamos de nos dedicar mais a isso, mas antes, temos um longo caminho a percorrer.
Continuaremos a fazer as coisas de qualquer maneira, mas as doações são uma das muitas formas de apoiar o que fazemos.
<span class="badge-opencollective"><a href="https://opencollective.com/npkill/contribute" title="Faça uma doação para este projeto usando o Open Collective"><img src="https://img.shields.io/badge/open%20collective-donate-green.svg" alt="Open Collective donate button" /></a></span>
### Obrigado!!
## Um enorme agradecimento aos nossos apoiadores :heart:
<a href="https://opencollective.com/npkill#backers" target="_blank"><img width="535" src="https://opencollective.com/npkill/tiers/backer.svg?width=535"></a>
---
### via Crypto
- btc: 1ML2DihUoFTqhoQnrWy4WLxKbVYkUXpMAX
- bch: 1HVpaicQL5jWKkbChgPf6cvkH8nyktVnVk
- eth: 0x7668e86c8bdb52034606db5aa0d2d4d73a0d4259
<a name="license"></a>
# :scroll: Licença
MIT © [Nya García Gallardo](https://github.com/NyaGarcia) e [Juan Torres Gómez](https://github.com/zaldih)
:cat::baby_chick:
---
================================================
FILE: README.tr.md
================================================
<p align="center">
<img src="https://npkill.js.org/img/npkill-text-outlined.svg" width="320" alt="npkill logo" />
<img src="https://npkill.js.org/img/npkill-scope-mono.svg" width="50" alt="npkill logo scope" />
</p>
<p align="center">
<img alt="npm" src="https://img.shields.io/npm/dy/npkill.svg">
<a href="#donations"><img src="https://img.shields.io/badge/donate-<3-red" alt="Donations Badge"/></a>
<img alt="npm version" src="https://img.shields.io/npm/v/npkill.svg">
<img alt="NPM" src="https://img.shields.io/npm/l/npkill.svg">
</p>
### Eski ve büyük <font color="red">**node_modules**</font> klasörlerini kolayca bulun ve **silin** :sparkles:
<p align="center">
<img src="/docs/npkill-demo-0.10.0.gif" alt="npkill demo GIF" />
</p>
Bu araç, sisteminizdeki tüm _node_modules_ dizinlerini ve kapladıkları alanı listelemenizi sağlar. Daha sonra, hangilerini silmek istediğinizi seçerek yer açabilirsiniz. Yaşasın!
## i18n
Npkill dokümantasyonunu uluslararası hale getirmek için çaba gösteriyoruz. İşte mevcut çevirilerin listesi:
- [Endonezce](./README.id.md)
- [İspanyolca](./README.es.md)
- [Portekizce](./README.pt.md)
- [Türkçe](./README.tr.md)
## İçindekiler
- [Özellikler](#features)
- [Kurulum](#installation)
- [Kullanım](#usage)
- [Seçenekler](#options)
- [Örnekler](#examples)
- [Yerel Kurulum](#setup-locally)
- [Yol Haritası](#roadmap)
- [Bilinen Hatalar](#known-bugs)
- [Katkıda Bulunma](#contributing)
- [Kahve Ismarlayın](#donations)
- [Lisans](#license)
<a name="features"></a>
# :heavy_check_mark: Özellikler
- **Alan Açın:** Makinenizde birikmiş, eski ve tozlu _node_modules_ klasörlerinden kurtulun.
- **Son Çalışma Alanı Kullanımı**: Çalışma alanındaki bir dosyayı en son ne zaman değiştirdiğinizi kontrol edin (bu, **last_mod** sütununda gösterilir).
- **Çok Hızlı:** NPKILL TypeScript ile yazılmıştır, ancak aramalar düşük seviyede gerçekleştirilerek performans büyük ölçüde artırılır.
- **Kullanımı Kolay:** Uzun komutlara elveda deyin. NPKILL kullanmak, node_modules listenizi okumak ve silmek için Del tuşuna basmak kadar basittir. Daha kolay olabilir mi? ;)
- **Düşük Bağımlılık:** Hiçbir bağımlılığı yok denecek kadar az.
<a name="installation"></a>
# :cloud: Kurulum
Kullanmak için gerçekten yüklemenize gerek yok!
Basitçe aşağıdaki komutu kullanabilirsiniz:
```bash
$ npx npkill
```
Ya da herhangi bir nedenle gerçekten yüklemek isterseniz:
```bash
$ npm i -g npkill
# Unix kullanıcılarının komutu sudo ile çalıştırması gerekebilir. Dikkatli olun.
```
> NPKILL, Node 14’ten düşük sürümleri desteklemiyor. Eğer bu durum sizi etkiliyorsa, `npkill@0.8.3` sürümünü kullanabilirsiniz.
<a name="usage"></a>
# :clipboard: Kullanım
```bash
$ npx npkill
# Ya da global olarak yüklüyse sadece npkill kullanabilirsiniz.
```
Varsayılan olarak, npkill `npkill` komutunun çalıştırıldığı dizinden başlayarak node_modules klasörlerini tarar.
Listelenen klasörler arasında <kbd>↓</kbd> ve <kbd>↑</kbd> tuşlarıyla gezinebilir, seçili klasörü silmek için <kbd>Space</kbd> veya <kbd>Del</kbd> tuşlarını kullanabilirsiniz.
Ayrıca sonuçlar arasında gezinmek için <kbd>j</kbd> ve <kbd>k</kbd> tuşlarını da kullanabilirsiniz.
Seçili sonucun bulunduğu klasörü açmak için <kbd>o</kbd> tuşuna basabilirsiniz.
Çıkmak için, <kbd>Q</kbd> ya da <kbd>Ctrl</kbd> + <kbd>C</kbd>.
**Önemli!** Sisteme kurulu bazı uygulamaların çalışması için node_modules klasörüne ihtiyacı vardır ve bu klasörlerin silinmesi uygulamaların bozulmasına yol açabilir. NPKILL, dikkatli olmanız için bu klasörleri :warning: simgesiyle vurgulayacaktır.
<a name="options"></a>
## Seçenekler
| ARGÜMAN | AÇIKLAMA |
| -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| -c, --bg-color | Satır vurgulama rengini değiştirin. _(Mevcut seçenekler: **mavi**, cam göbeği, eflatun, beyaz, kırmızı ve sarı)_ |
| -d, --directory | Aramaya başlanacak dizini ayarlayın. Varsayılan başlangıç noktası . olarak belirlenmiştir. |
| -D, --delete-all | Bulunan tüm node_modules klasörlerini otomatik olarak siler. `-x` ile birlikte kullanılması önerilir. |
| -e, --hide-errors | Varsa hataları gizler |
| -E, --exclude | Aramadan hariç tutulacak dizinleri belirtin (dizin listesi çift tırnak içinde "", dizinler virgülle ',' ayrılmalıdır). Örnek: "ignore1, ignore2" |
| -f, --full | Aramaya kullanıcının ev dizininden başlayın (örneğin Linux'ta "/home/user"). |
| -gb | Klasörleri Megabytes yerine Gigabytes olarak göster. |
| -h, --help, ? | Bu yardım sayfasını göster ve çık. |
| -nu, --no-check-update | Başlangıçta güncellemeleri kontrol etme. |
| -s, --sort | Sonuçları şu kriterlere göre sırala: `size`, `path` veya `last-mod` |
| -t, --target | Aramak istediğiniz dizinlerin adını belirtin (varsayılan olarak node_modules). |
| -x, --exclude-hidden-directories | Gizli dizinleri ("nokta" dizinleri) arama kapsamı dışında bırak. |
| --dry-run | Hiçbir şeyi silmez (rastgele bir gecikme ile simüle eder). |
| -v, --version | npkill sürümünü gösterir. |
**Uyarı:** _Gelecek sürümlerde bazı komutlar değişebilir_
<a name="examples"></a>
## Örnekler
- _projects_ dizininizdeki **node_modules** klasörlerini arayın:
```bash
npkill -d ~/projects
# diğer alternatif:
cd ~/projects
npkill
```
- "dist" adlı dizinleri listeleyin ve hata oluşursa gösterin.
```bash
npkill --target dist -e
```
- Mor renkli imleç gösterilir... çünkü moru seviyorum!
```bash
npkill --color magenta
```
- _projects_ dizininizdeki **vendor** klasörlerini listeleyin, boyuta göre sırala ve boyutları GB cinsinden göster:
```bash
npkill -d '~/more projects' -gb --sort size --target vendor
```
- _projects_ dizininizdeki **node_modules** klasörlerini listeleyin, ancak _progress_ ve _ignore-this_ dizinlerindeki klasörleri hariç tutun:
```bash
npkill -d 'projects' --exclude "progress, ignore-this"
```
- Yedeklerinize gizlice karışmış tüm node_modules klasörlerini otomatik olarak silin:
```bash
npkill -d ~/backups/ --delete-all
```
<a name="setup-locally"></a>
# :pager: Yerel Kurulum
```bash
# -- Öncelikle, repoyu klonlayın.
git clone https://github.com/voidcosmos/npkill.git
# -- Dizin içine gidin
cd npkill
# -- Bağımlılıkları yükleyin
npm install
# -- Ve çalıştırın!
npm run start
# -- Eğer bazı parametrelerle çalıştırmak istiyorsanız, aşağıdaki örnekte olduğu gibi "--" eklemeniz gerekir:
npm run start -- -f -e
```
<a name="roadmap"></a>
# :crystal_ball: Yol Haritası
- [x] 0.1.0 yayınla!
- [x] Kodu geliştir
- [x] Performansı iyileştir
- [ ] Performansı daha da iyileştir!
- [x] Sonuçları boyuta ve yola göre sırala
- [x] Diğer türde dizinlerin (hedeflerin) aranmasına izin ver
- [ ] Daha minimalist bir modül olması için bağımlılıkları azalt
- [ ] Belirli bir süredir kullanılmayan dizinlere göre filtreleme yapmaya izin ver
- [ ] Dizinleri ağaç biçiminde göstermek için bir seçenek oluştur
- [x] Bazı menüler ekle
- [x] Log servisi ekle
- [ ] Periyodik ve otomatik temizlik (?)
<a name="known-bugs"></a>
# :bug: Bilinen Hatalar :bug:
- Bazen klasör silinirken CLI kilitlenebilir.
- TTY kullanmayan bazı terminaller (örneğin Windows’taki Git Bash) çalışmaz.
- Özellikle yol (path) bazında sıralama, çok sayıda olduğunda terminali yavaşlatabilir.
- Bazen, boyut hesaplamaları olması gerekenden daha yüksek çıkabilir.
- (ÇÖZÜLDÜ) Yüksek seviyeli dizinlerden (örneğin Linux'taki / dizini) arama yaparken performans sorunları yaşanabilir.
- (ÇÖZÜLDÜ) Bazen CLI güncellenirken metinler bozuluyor.
- (ÇÖZÜLDÜ) Dizinlerin boyutunu analiz etmek olması gerekenden daha uzun sürüyor.
> Eğer herhangi bir hata bulursanız, çekinmeden bir issue açın :)
<a name="contributing"></a>
# :revolving_hearts: Katkıda Bulunma
Katkıda bulunmak isterseniz [CONTRIBUTING.md](.github/CONTRIBUTING.md) dosyasını inceleyin.
<a name="donations"></a>
# :coffee: Bize bir kahve ısmarlayın
<img align="right" width="300" src="https://npkill.js.org/img/cat-donation-cup.png">
Boş zamanlarımızda, programlama sektörüne olan tutkumuz nedeniyle npkill'i geliştirdik.
Gelecekte, tamamen buna odaklanmak istiyoruz ama önümüzde uzun bir yol var.
Yine de işlerimizi yapmaya devam edeceğiz, ancak bağışlar yaptığımız işi desteklemenin birçok yolundan sadece biridir.
<span class="badge-opencollective"><a href="https://opencollective.com/npkill/contribute" title="Donate to this project using Open Collective"><img src="https://img.shields.io/badge/open%20collective-donate-green.svg" alt="Open Collective donate button" /></a></span>
### Teşekkürler!!
## Destekçilerimize kocaman teşekkürler :heart:
<a href="https://opencollective.com/npkill#backers" target="_blank"><img width="535" src="https://opencollective.com/npkill/tiers/backer.svg?width=535"></a>
---
### Kripto alternatifi
- btc: 1ML2DihUoFTqhoQnrWy4WLxKbVYkUXpMAX
- bch: 1HVpaicQL5jWKkbChgPf6cvkH8nyktVnVk
- eth: 0x7668e86c8bdb52034606db5aa0d2d4d73a0d4259
<a name="license"></a>
# :scroll: Lisans
MIT © [Nya García Gallardo](https://github.com/NyaGarcia) and [Juan Torres Gómez](https://github.com/zaldih)
:cat::baby_chick:
---
================================================
FILE: docs/RELEASE.md
================================================
# How to release a new version
### 1. Ensure the latest changes are available
```bash
git checkout develop
git pull
git checkout main
git pull
```
### 2. Merge develop into main
```bash
git merge develop --no-ff
```
### 3. Run the release script...
```bash
# Ensure that the dependencies match those in package.json
rm -rf node_modules; npm i
npm run release
```
The release script takes care of 2 things:
- Execute the compilation tasks (`npm run build`).
- Start the interactive release process itself.
### 4. Pick version type (major, minor, path)
### 5. Test the new release.
================================================
FILE: docs/create-demo.sh
================================================
#!/bin/bash
#
# This script create a example node_modules files
# only for demo purpose.
#
BASE_PATH="$HOME/allStartHere"
function create(){
projectName=$1
fileSize=$2
fakeModificationDate=$(expr $(date +"%s") - $(shuf -i 0-5259486 -n 1)) # 2 month of margin
mkdir -p "$BASE_PATH/$projectName/node_modules"
head -c ${fileSize}MB /dev/zero > "$BASE_PATH/$projectName/node_modules/a"
touch -a -m -d @$fakeModificationDate "$BASE_PATH/$projectName/sample_npkill_file"
}
create 'secret-project' '58'
create 'Angular Tuto' '812'
create 'testest' '43'
create 'archived/Half Dead 3' '632'
create 'cats' '384'
create 'navigations/001' '89'
create 'navigations/002' '88'
create 'navigations/003' '23'
create 'more-cats' '371'
create 'projects/hero-sample' '847'
create 'projects/awesome-project' '131'
create 'projects/calculator/frontend' '883'
create 'projects/caluclator/backend' '244'
create 'games/buscaminas' '349'
create 'games/archived/cards' '185'
create 'archived/weather-api' '151'
create 'kiwis-are-awesome' '89'
create 'projects/projects-of-projects/trucs' '237'
create 'projects/projects-of-projects/conversor-divisas' '44'
create 'projects/vue/hello-world' '160'
create 'projects/vue/Quantic stuff' '44'
================================================
FILE: docs/json-output.md
================================================
# JSON Output
Npkill supports two JSON output modes that allow you to integrate it into automation scripts, monitoring systems, or other tools.
## Table of Contents
- [Output Modes](#output-modes)
- [JSON Structure](#json-structure)
- [Examples](#examples)
- [TypeScript Interfaces](#typescript-interfaces)
- [Use Cases](#use-cases)
## Output Modes
### Simple JSON (`--json`)
The `--json` option collects all results and outputs them as a single JSON object at the end of the scan. This is useful when you need all results at once for batch processing.
```bash
npkill --json
```
### Streaming JSON (`--json-stream`)
The `--json-stream` option outputs each result as a separate JSON object on its own line as soon as it's found. This is useful for real-time processing or when dealing with large scans where you want to start processing results immediately.
```bash
npkill --json-stream
```
## JSON Structure
### Simple JSON Output
The simple JSON format includes all results in a single object with metadata:
```json
{
"version": 1,
"results": [
{
"path": "/home/user/project1/node_modules",
"size": 157286400,
"modificationTime": 1640995200000,
"riskAnalysis": {
"isSensitive": false
}
},
{
"path": "/home/user/project2/node_modules",
"size": 89478400,
"modificationTime": 1640995300000
}
],
"meta": {
"resultsCount": 2,
"runDuration": 1523
}
}
```
### Streaming JSON Output
Each line in streaming mode contains a single result:
```json
{"version":1,"result":{"path":"/home/user/project1/node_modules","size":157286400,"modificationTime":1640995200000,"riskAnalysis":{"isSensitive":false}}}
{"version":1,"result":{"path":"/home/user/project2/node_modules","size":89478400,"modificationTime":1640995300000}}
```
### Error Output
Errors are output to stderr in JSON format:
```json
{
"version": 1,
"error": true,
"message": "Permission denied accessing /restricted/path",
"timestamp": "1640995300000"
}
```
### Field Descriptions
- **`version`**: Schema version.
- **`path`**: Absolute path to the found directory.
- **`size`**: Directory size in bytes.
- **`modificationTime`**: Unix timestamp (milliseconds) of the most recently modified file.
- **`riskAnalysis`**: Optional risk assessment for deletion
- **`isSensitive`**: Whether the directory might be important for system functionality.
- **`reason`**: Human-readable explanation of the risk assessment.
- **`resultsCount`**: Total number of results found.
- **`runDuration`**: Total scan time in milliseconds.
## Examples
### Basic Usage
```bash
# Get all results as JSON
npkill --json > results.json
# Stream results in real-time
npkill --json-stream | while read line; do
echo "Found: $(echo $line | jq -r '.result.path')"
done
```
### Using with jq for Processing
```bash
# Extract only paths larger than 100MB
npkill --json | jq '.results[] | select(.size > 104857600) | .path'
# Count total size of all node_modules
npkill --json | jq '.results | map(.size) | add'
# Get the 5 largest directories
npkill --json | jq '.results | sort_by(.size) | reverse | .[0:5] | .[] | "\(.size | tostring) bytes: \(.path)"'
# Convert streaming output to a valid JSON array
npkill --json-stream | jq -s 'map(.result)'
```
### Error Handling
```bash
# Save results to file, ignore errors
npkill --json 2>/dev/null > results.json
# Save both results and errors to separate files
npkill --json-stream > results.jsonl 2> errors.jsonl
# Process only successful results in streaming mode
npkill --json-stream 2>/dev/null | jq -r '.result.path'
```
### Automation Examples
```bash
# Find and delete directories older than 30 days
npkill --json | jq -r '.results[] | select(.modificationTime < (now - 2592000) * 1000) | .path' | while read dir; do
echo "Deleting old directory: $dir"
rm -rf "$dir"
done
# Generate a report of space usage
npkill --json | jq -r '.results[] | "\(.path): \(.size / 1048576 | floor)MB"' > space-report.txt
# Monitor in real-time and alert on large directories
npkill --json-stream | jq -r 'select(.result.size > 524288000) | "LARGE DIR: \(.result.path) (\(.result.size / 1048576 | floor)MB)"'
```
### Integration with Other Tools
```bash
# Send results to a monitoring system
npkill --json-stream | while read line; do
curl -X POST -H "Content-Type: application/json" -d "$line" http://monitoring-system/api/npkill
done
# Create a CSV report
echo "Path,Size(MB),LastModified,Status" > report.csv
npkill --json | jq -r '.results[] | "\(.path),\(.size/1048576|floor),\(.modificationTime),\(.status)"' >> report.csv
# Filter and format for human reading
npkill --json | jq -r '.results[] | select(.size > 52428800) | "📁 \(.path)\n 💾 Size: \(.size/1048576|floor)MB\n 📅 Modified: \(.modificationTime | strftime("%Y-%m-%d %H:%M:%S"))\n"'
```
## Interfaces
```typescript
interface JsonOutputBase {
version: number;
}
interface JsonStreamOutput extends JsonOutputBase {
result: CliScanFoundFolder;
}
interface JsonSimpleOutput extends JsonOutputBase {
results: CliScanFoundFolder[];
meta: {
resultsCount: number;
runDuration: number; // milliseconds
};
}
interface JsonErrorOutput extends JsonOutputBase {
error: true;
message: string;
timestamp: string;
}
interface CliScanFoundFolder {
path: string;
size: number; // bytes
modificationTime: number; // Unix timestamp
riskAnalysis?: {
isSensitive: boolean;
reason?: string;
};
}
```
================================================
FILE: docs/npkillrc.md
================================================
# Config File - npkillrc
You can customize the behavior of npkill through the config file (`.npkillrc` by default).
## Table of Contents
- [Location](#location)
- [Example](#example)
- [Options](#options)
- [rootDir](#rootDir)
- [exclude](#exclude)
- [sortBy](#sortby)
- [sizeUnit](#sizeunit)
- [excludeSensitiveResults](#excludeSensitiveResults)
- [dryRun](#dryrun)
- [checkUpdates](#checkupdates)
- [defaultProfiles](#defaultProfiles)
- [profiles](#profiles)
- [Error Handling](#error-handling)
- [Testing Your Configuration](#testing-your-configuration)
## Location
Npkill searches for the configuration file in the following order of priority:
1. **Custom path** specified via `--config` flag
2. **Current working directory**: `./.npkillrc`
3. **User's home directory**: `~/.npkillrc`
The first configuration file found will be used.
You can use `--config` in this way:
```bash
npkill --config /path/to/your/config.json
```
## Example
```json
{
"rootDir": "/home/user/projects",
"exclude": ["important-project", "production-app"],
"sortBy": "size",
"sizeUnit": "auto",
"excludeSensitiveResults": true,
"dryRun": false,
"checkUpdates": true,
"defaultProfiles": ["node", "database"],
"profiles": {
"webdev": {
"description": "Frontend web development artifacts and build outputs",
"targets": ["dist", ".next", ".nuxt", ".output", "build", ".svelte-kit"]
},
"mobile": {
"description": "Mobile platform build folders and caches",
"targets": ["Pods", "build", "DerivedData", ".gradle", "android/build"]
},
"database": {
"description": "Database data folders (use with caution)",
"targets": ["data", "db", "mongodb", "postgres"]
}
}
}
```
## Options
### rootDir
**Type:** `string`
**Default:** ``
Absolute path from which the search will begin.
```json
"rootdir": "/home/user/my-projects/"
```
### exclude
**Type:** `string[]`
**Default:** `[]`
Array of directory names to exclude from search. Npkill will skip these directories and their subdirectories.
```json
"exclude": ["production-project", "node_modules/.cache"]
```
### sortBy
**Type:** `"none" | "size" | "path" | "age"`
**Default:** `"none"`
Default sort order for results.
- `"none"`: Results appear in the order they're found
- `"size"`: Largest folders first
- `"path"`: Alphabetical by path
- `"age"`: Oldest modified projects first
```json
"sortBy": "size"
```
### sizeUnit
**Type:** `"auto" | "mb" | "gb"`
**Default:** `"auto"`
Unit for displaying folder sizes.
- `"auto"`: Sizes < 1024MB shown in MB, larger sizes in GB
- `"mb"`: Always show in megabytes
- `"gb"`: Always show in gigabytes
```json
"sizeUnit": "auto"
```
### excludeSensitiveResults
**Type:** `boolean`
**Default:** `false`
Hide results that may be sensitive.
```json
"excludeSensitiveResults": true
```
### dryRun
**Type:** `boolean`
**Default:** `false`
When true, deletions are simulated (nothing is actually deleted).
```json
"dryRun": false
```
### checkUpdates
**Type:** `boolean`
**Default:** `true`
Check for updates on startup.
```json
"checkUpdates": true
```
### defaultProfiles
**Type:** `string[]`
**Default:** `["node"]`
Define the profile names to be used by default. These can be either built-in or user-defined names.
```json
"checkUpdates": true
```
### profiles
**Type:** `{ [name: string]: { targets: string[] } }`
**Default:** `{}`
Define custom profiles with specific target directories. These can be used with the `-p` or `--profiles` flag.
These will overwrite the base profiles.
You can check the existing ones with `--profiles` and even copy the output of those you are interested in to combine them into one.
```json
"profiles": {
"webdev": {
"description": "Frontend web development artifacts and build outputs",
"targets": ["dist", ".next", ".nuxt", ".output"]
},
"mystack": {
"description": "Full-stack project artifacts (JS/Python/Java)",
"targets": ["venv", ".venv", "target", "__pycache__", ".gradle"]
},
"mobile": {
"description": "Mobile platform build folders and caches",
"targets": ["Pods", "build", "DerivedData", "gradle"]
}
}
```
## Error Handling
Npkill will check that the configuration file is correct at each startup. This includes:
- **Unknown properties**.
- **Type checking**.
- **Value validation**.
### Testing Your Configuration
To test if your `.npkillrc` is valid, simply run npkill:
To check that a file is valid, simply run npkill as usual. If there is an error, you will be informed exactly what the problem is.
================================================
FILE: docs/profiles.md
================================================
# Profiles
This document defines built-in profiles for npkill. A profile is a named preset of "safe-to-delete" directories for a given ecosystem.
While these directories are **generally safe to delete**, it all depends on their context. Therefore, it is important to verify the result shown before deleting it. However, we have tried to maintain a conservative list.
- Profiles are opt-in via `--profiles` (comma-separated). Example: `--profiles node,python`.
- Only directory base names are matched (the last path segment) (more advanced heuristics will be implemented in the future).
- All targets below are rebuildable caches, dependencies, or compiled outputs. So doesnt should have any problem deleting it. But before delete, peek if is secure to remove for your case.
Default behavior
- By default (no `--profiles`), npkill use the `node` profile.
Special profile: all
- `all` includes every target listed in all profiles below. Use with care if you want a full clean sweep.
## node (default)
- `node_modules`: Node.js project dependencies. Deleting forces a full reinstallation. Regenerated by running `npm install`, `yarn`, or `pnpm install`.
- `.npm`: npm's package cache. Deleting may slow down the next install. Regenerated automatically by npm on subsequent installs.
- `.pnpm-store`: pnpm's global content-addressable store. Deleting removes all shared packages. Regenerated by pnpm on subsequent installs.
- `.yarn/cache`: Yarn v2+ local project cache. Deleting requires re-downloading packages. Regenerated by `yarn install`.
- `.next`: Next.js build and cache directory. Deleting clears build artifacts and cache. Regenerated on the next `next dev` or `next build`.
- `.nuxt`: Nuxt.js build output directory. Deleting removes the generated application. Regenerated by running `nuxt build`.
- `.angular`: Angular CLI cache and metadata. Deleting is safe and may resolve caching issues. Regenerated by the Angular CLI during the next build or serve command.
- `.svelte-kit`: SvelteKit build and cache directory. Deleting removes the generated application and cache. Regenerated on the next build or dev server run.
- `.vite`: Vite's pre-bundled dependency cache. Deleting forces Vite to re-bundle dependencies on next startup. Regenerated automatically by Vite.
- `.nx`: Nx workspace computation cache. Deleting results in a slower, non-cached build next time. Regenerated by Nx on subsequent task executions.
- `.turbo`: Turborepo's local cache. Deleting forces a full execution of all tasks on the next run. Regenerated by Turborepo on subsequent `turbo run` commands.
- `.parcel-cache`: Parcel bundler's cache. Deleting may slow down the next build. Regenerated automatically by Parcel.
- `.rpt2_cache`: Cache for `rollup-plugin-typescript2`. Deleting forces a full TypeScript re-compilation. Regenerated on the next Rollup build.
- `.eslintcache`: ESLint's cache for changed files. Deleting forces a full linting process. Regenerated by ESLint when run with the `--cache` flag.
- `.esbuild`: esbuild's build cache. Deleting may slow down builds that use esbuild. Regenerated automatically by esbuild.
- `.cache`: Generic cache directory for various tools. Deleting is generally safe but may slow down the tools that use it. Regenerated automatically by the respective tools.
- `.rollup.cache`: Rollup's build cache. Deleting may slow down the next build. Regenerated on the next Rollup build if caching is enabled.
- `storybook-static`: Static build output for a Storybook. Deleting removes the deployed Storybook. Regenerated by running `build-storybook`.
- `coverage`: Code coverage reports. Deleting removes historical coverage data. Regenerated by running tests with coverage enabled.
- `.nyc_output`: Raw coverage output from `nyc`. Deleting removes raw coverage data. Regenerated on the next run of `nyc`.
- `.jest`: Jest's test cache and artifacts. Deleting may slow down the next test run. Regenerated automatically by Jest.
- `gatsby_cache`: Gatsby's internal cache. Deleting may slow down the next build. Regenerated automatically by Gatsby.
- `.docusaurus`: Docusaurus build cache and data. Deleting removes generated site files. Regenerated by Docusaurus on the next build.
- `.swc`: SWC (Speedy Web Compiler) cache. Deleting may slow down the next compilation. Regenerated automatically by SWC.
- `.stylelintcache`: Stylelint's cache for linted files. Deleting forces a full re-lint. Regenerated by Stylelint when run with the `--cache` flag.
- `deno_cache`: Deno's cache for modules. Deleting is safe. Regenerated when Deno modules are next fetched.
## python
- `__pycache__`: Python bytecode files. Deleting is safe as they are regenerated by Python automatically.
- `.pytest_cache`: pytest's cache for test results and metadata. Deleting may slow down the next test run. Regenerated automatically by pytest.
- `.mypy_cache`: mypy's cache for type-checking results. Deleting forces a full re-check. Regenerated automatically by mypy.
- `.ruff_cache`: Ruff linter's cache. Deleting forces a full re-lint. Regenerated automatically by Ruff.
- `.tox`: tox's virtual environments and test artifacts. Deleting removes isolated testing environments. Regenerated by running `tox`.
- `.nox`: nox's virtual environments and session data. Deleting removes isolated session environments. Regenerated by running `nox`.
- `.pytype`: pytype's cache for static analysis. Deleting forces a full re-analysis. Regenerated automatically by pytype.
- `.pyre`: Pyre type checker's cache. Deleting forces a full re-check. Regenerated automatically by Pyre.
- `htmlcov`: HTML code coverage reports. Deleting removes historical coverage data. Regenerated by running coverage tools (e.g., `coverage html`).
- `.venv`: Python virtual environment. Deleting removes all installed packages and the isolated environment. Regenerated by creating a new virtual environment (e.g., `python -m venv .venv`).
- `venv`: Same as `.venv`. Deleting removes the virtual environment. Regenerated similarly.
## data-science
- `.ipynb_checkpoints`: Jupyter Notebook's auto-save checkpoints. Deleting removes recovery points for notebooks. Regenerated automatically by Jupyter.
- `__pycache__`: Python bytecode files. Deleting is safe. Regenerated automatically by Python.
- `.venv` / `venv`: Python virtual environment. Deleting removes the isolated environment and its packages. Regenerated by creating a new virtual environment.
- `outputs/`: A common directory for auto-generated files, such as models or plots. Verify contents before deleting. Regeneration depends on the specific script or tool that created it.
- `.dvc`: Data Version Control (DVC) cache and metadata. Deleting can lead to data loss if not properly managed. Regenerated by DVC commands like `dvc repro`.
- `.mlruns`: MLflow experiment tracking logs. Deleting removes experiment history. Regenerated when you run new MLflow experiments.
## java
- `target`: Maven's build output directory. Deleting removes all compiled code, packages, and artifacts. Regenerated by running `mvn package` or other Maven build commands.
- `.gradle`: Gradle's cache and wrapper files within a project. Deleting may slow down the next build. Regenerated automatically by Gradle.
- `out`: Default output directory for some IDEs like IntelliJ IDEA. Deleting removes compiled classes. Regenerated on the next build.
- Warning: `out` is a generic folder name in various ecosystems.
## android
- `.cxx`: Android NDK build cache. Deleting may slow down native builds. Regenerated by the Android Gradle plugin during the next build.
- `externalNativeBuild`: External NDK build artifacts. Deleting removes intermediate native build files. Regenerated on the next native build.
## swift
- `DerivedData`: Xcode's build artifacts, indexes, and cache. Deleting is a common troubleshooting step and clears the build cache. Regenerated by Xcode on the next build.
- `.swiftpm`: Swift Package Manager's cache and build data. Deleting removes local package caches and build artifacts. Regenerated on the next `swift build` or resolve.
## dotnet
- `obj`: Intermediate object files from the build process. Deleting forces a full recompile. Regenerated by the .NET compiler (e.g., `dotnet build`).
- `TestResults`: Test output and reports. Deleting removes historical test data. Regenerated by running tests (e.g., `dotnet test`).
- `.vs`: Visual Studio's local workspace data, including user-specific settings and cache. Deleting is generally safe. Regenerated when you open the solution in Visual Studio.
## rust
- `target`: Cargo's build output directory. Deleting removes all compiled artifacts. Regenerated by running `cargo build`.
## ruby
- `.bundle`: Bundler's settings and cache for the project. Deleting is safe. Regenerated by `bundle install`.
## elixir
- `_build`: Mix's build output directory. Deleting removes compiled artifacts. Regenerated by `mix compile`.
- `deps`: Project dependencies. Deleting requires re-fetching all dependencies. Regenerated by `mix deps.get`.
- Warning: `deps/` is a generic name that may appear in non-Elixir projects too.
- `cover`: Test coverage reports. Deleting removes historical coverage data. Regenerated by running tests with coverage enabled.
## haskell
- `dist-newstyle`: Cabal's build output and cache. Deleting removes compiled files. Regenerated by `cabal build`.
- `.stack-work`: Stack's build cache and artifacts. Deleting removes compiled files and intermediate data. Regenerated by `stack build`.
## scala
- `.bloop`: Bloop build server's metadata and configuration. Deleting may require re-importing the project in your IDE. Regenerated by Bloop.
- `.metals`: Metals IDE's cache and build data. Deleting forces a re-index of the project. Regenerated when you open the project in a Metals-enabled editor.
- `target`: sbt's build output directory. Deleting removes compiled code and artifacts. Regenerated by sbt build commands.
## cpp
- `CMakeFiles`: CMake's intermediate build files. Deleting forces CMake to regenerate the build environment. Regenerated by running `cmake`.
- `cmake-build-debug`: CLion or CMake's debug build output. Deleting removes debug binaries. Regenerated on the next debug build.
- `cmake-build-release`: CLion or CMake's release build output. Deleting removes release binaries. Regenerated on the next release build.
## unity
- `Library`: Unity's cache of imported assets and metadata. Deleting is safe but forces a full re-import of all project assets, which can be time-consuming. Regenerated when Unity opens the project.
- Warning: On macOS, this can match the critical `~/Library` system folder.
- `Temp`: Temporary build files. Deleting is safe when the Unity editor is closed. Regenerated during the next build process.
- `Obj`: Intermediate object files from the build process. Deleting forces a full recompile. Regenerated on the next build.
- Warning: Generic name; may appear in unrelated toolchains.
## unreal
- `Intermediate`: Intermediate build files. Deleting is safe and forces a regeneration of these files. Regenerated by the Unreal Engine build system.
- Warning: Generic name; ensure it’s an Unreal project context.
- `DerivedDataCache`: Cache for derived asset data. Deleting forces assets to be re-cooked. Regenerated on demand by the engine.
- `Binaries`: Compiled binaries and libraries. Deleting removes executable files. Regenerated by the build system.
## godot
- `.import`: Godot's cache of imported assets. Deleting forces a re-import of all assets. Regenerated when the Godot editor is opened.
- `.godot`: Godot's project cache and metadata. Deleting is generally safe. Regenerated by the Godot editor.
## infra
- `.serverless`: Serverless Framework's deployment artifacts. Deleting removes the packaged service. Regenerated on the next `serverless package` or `deploy`.
- `.vercel`: Vercel's local project data and deployment cache. Deleting is safe for local development. Regenerated by the Vercel CLI.
- `.netlify`: Netlify's local cache and configuration data. Deleting is safe for local development. Regenerated by the Netlify CLI.
- `.terraform`: Terraform's working directory for providers and modules. Deleting requires re-initialization. Regenerated by running `terraform init`.
- `.sass-cache`: Legacy cache for the Sass compiler. Deleting is safe. Regenerated by the Sass compiler if still in use.
- `.cpcache`: Clojure CLI's compilation cache. Deleting forces a re-compilation of Clojure sources. Regenerated on the next execution.
- `elm_stuff`: Elm's dependency cache and build artifacts. Deleting requires re-downloading dependencies. Regenerated by `elm make` or `elm reactor`.
- `nimcache`: Nim compiler's cache. Deleting may slow down the next compilation. Regenerated automatically by the Nim compiler.
- `deno_cache`: Deno's cache for modules. Deleting is safe. Regenerated when Deno modules are next fetched.
================================================
FILE: eslint.config.mjs
================================================
// @ts-check
import eslint from '@eslint/js';
import tseslint from 'typescript-eslint';
import nodePlugin from 'eslint-plugin-n';
import pluginPromise from 'eslint-plugin-promise';
import eslintConfigPrettier from 'eslint-config-prettier/flat';
export default tseslint.config(
eslint.configs.recommended,
tseslint.configs.recommended,
nodePlugin.configs['flat/recommended'],
pluginPromise.configs['flat/recommended'],
eslintConfigPrettier,
{
ignores: [
'node_modules',
'lib',
'dist',
'build',
'**/*.min.js',
'/src/index.ts',
],
rules: {
quotes: ['error', 'single'],
},
},
);
================================================
FILE: jest.config.ts
================================================
import type { JestConfigWithTsJest } from 'ts-jest';
const config: JestConfigWithTsJest = {
preset: 'ts-jest/presets/default-esm',
testEnvironment: 'node',
extensionsToTreatAsEsm: ['.ts'],
testRegex: '(/tests/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$',
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
moduleNameMapper: {
'^(\\.{1,2}/.*)\\.js$': '$1',
},
// moduleNameMapper: {
// '^@core/(.*)$': '<rootDir>/src/$1',
// '^@services/(.*)$': '<rootDir>/src/services/$1',
// '^@interfaces/(.*)$': '<rootDir>/src/interfaces/$1',
// '^@constants/(.*)$': '<rootDir>/src/constants/$1',
// },
// transform: {
// '^.+\\.(t|j)sx?$': ['ts-jest', { useESM: true }],
// },
transform: {
'^.+\\.(t|j)sx?$': ['ts-jest', { useESM: true }],
},
testPathIgnorePatterns: ['<rootDir>/.stryker*'],
};
export default config;
================================================
FILE: package.json
================================================
{
"name": "npkill",
"version": "0.12.2",
"description": "List any node_modules directories in your system, as well as the space they take up. You can then select which ones you want to erase to free up space.",
"exports": "./lib/index.js",
"type": "module",
"engines": {
"node": ">=18.18.0"
},
"publishConfig": {
"access": "public"
},
"bin": {
"npkill": "lib/index.js"
},
"author": "Nya Garcia & Juan Torres",
"repository": {
"type": "git",
"url": "https://github.com/zaldih/npkill"
},
"license": "MIT",
"keywords": [
"cli",
"free up space",
"npm",
"node",
"modules",
"clean",
"tool",
"delete",
"find",
"interactive"
],
"files": [
"lib/**/*"
],
"scripts": {
"build": "tsc",
"start": "node -r tsconfig-paths/register --loader ts-node/esm --no-warnings ./src/index.ts",
"test": "node --experimental-vm-modules --experimental-modules node_modules/jest/bin/jest.js",
"test:watch": "npm run test -- --watch",
"test:mutant": "stryker run",
"release": "npm run build && np",
"debug": "TS_NODE_FILES=true node --inspect -r ts-node/register ./src/index.ts",
"prepare": "husky install",
"format": "prettier --write .",
"lint": "eslint"
},
"dependencies": {
"ansi-escapes": "7.1.1",
"open-file-explorer": "1.0.2",
"picocolors": "1.1.1"
},
"devDependencies": {
"@commitlint/config-conventional": "20.0.0",
"@eslint/js": "9.38.0",
"@jest/globals": "30.2.0",
"@stryker-mutator/core": "9.2.0",
"@stryker-mutator/jest-runner": "9.2.0",
"@types/jest": "30.0.0",
"@types/node": "18.18.0",
"commitlint": "20.1.0",
"del": "8.0.1",
"eslint": "9.38.0",
"eslint-config-prettier": "10.1.8",
"eslint-plugin-n": "17.23.1",
"eslint-plugin-promise": "7.2.1",
"husky": "9.1.7",
"jest": "30.2.0",
"lint-staged": "15.5.2",
"np": "10.2.0",
"prettier": "3.6.2",
"rimraf": "5.0.10",
"ts-jest": "29.4.5",
"ts-node": "10.9.2",
"tslint": "6.1.3",
"typescript": "5.8.3",
"typescript-eslint": "8.46.2"
},
"peerDependencies": {
"rxjs": "^7.8.2"
},
"husky": {
"hooks": {
"commit-msg": "commitlint -E HUSKY_GIT_PARAMS",
"pre-commit": "lint-staged"
}
},
"commitlint": {
"extends": [
"@commitlint/config-conventional"
]
},
"lint-staged": {
"*.{js,ts,css,json,md}": [
"prettier --write"
]
},
"ethereum": "0x7668e86c8bdb52034606db5aa0d2d4d73a0d4259"
}
================================================
FILE: src/cli/cli.controller.ts
================================================
import {
ConsoleService,
ResultsService,
SpinnerService,
UpdateService,
} from './services/index.js';
import {
DEFAULT_CONFIG,
MIN_CLI_COLUMNS_SIZE,
UI_POSITIONS,
} from '../constants/index.js';
import { DEFAULT_PROFILE } from '../core/constants/profiles.constants.js';
import { ERROR_MSG, INFO_MSGS } from '../constants/messages.constants.js';
import { IConfig, CliScanFoundFolder, IKeyPress } from './interfaces/index.js';
import { firstValueFrom, Subject } from 'rxjs';
import { mergeMap, tap } from 'rxjs/operators';
import { COLORS } from '../constants/cli.constants.js';
import { FOLDER_SORT } from '../constants/sort.result.js';
import {
StatusUi,
StatsUi,
ResultsUi,
LogsUi,
InteractiveUi,
HelpCommandUi,
HeaderUi,
GeneralUi,
WarningUi,
OptionsUi,
HelpUi,
} from './ui/index.js';
import { MENU_BAR_OPTIONS } from './ui/components/header/header-ui.constants.js';
import { UiService } from './services/ui.service.js';
import _dirname from '../dirname.js';
import pc from 'picocolors';
import { homedir } from 'os';
import path from 'path';
import openExplorer from 'open-file-explorer';
import { Npkill, ConfigService, ProfilesService } from '../core/index.js';
import { LoggerService } from '../core/services/logger.service.js';
import { ScanStatus } from '../core/interfaces/search-status.model.js';
import { isSafeToDelete } from '../utils/is-safe-to-delete.js';
import { getFileContent } from '../utils/get-file-content.js';
import { ResultDetailsUi } from './ui/components/result-details.ui.js';
import { ScanService } from './services/scan.service.js';
import { JsonOutputService } from './services/json-output.service.js';
export class CliController {
private readonly config: IConfig = DEFAULT_CONFIG;
private searchStart: number;
private searchDuration: number;
private uiHeader: HeaderUi;
private uiGeneral: GeneralUi;
private uiStats: StatsUi;
private uiStatus: StatusUi;
private uiResults: ResultsUi;
private uiLogs: LogsUi;
private uiWarning: WarningUi;
private activeComponent: InteractiveUi | null = null;
constructor(
private readonly stdout: NodeJS.WriteStream,
private readonly npkill: Npkill,
private readonly logger: LoggerService,
private readonly searchStatus: ScanStatus,
private readonly resultsService: ResultsService,
private readonly spinnerService: SpinnerService,
private readonly consoleService: ConsoleService,
private readonly updateService: UpdateService,
private readonly uiService: UiService,
private readonly scanService: ScanService,
private readonly jsonOutputService: JsonOutputService,
private readonly profilesService: ProfilesService,
private readonly configService: ConfigService,
) {}
init(): void {
this.logger.info(`Npkill CLI started! v${this.getVersion()}`);
this.loadConfigFile();
this.parseArguments();
if (this.config.jsonStream) {
this.logger.info('JSON stream mode enabled.');
this.setupJsonModeSignalHandlers();
this.scan();
return;
}
if (this.config.jsonSimple) {
this.logger.info('JSON simple mode enabled.');
this.setupJsonModeSignalHandlers();
this.scan();
return;
}
this.initUi();
if (this.consoleService.isRunningBuild()) {
this.uiHeader.programVersion = this.getVersion();
}
this.consoleService.startListenKeyEvents();
this.checkRequirements();
this.prepareScreen();
this.setupEventsListener();
if (this.config.checkUpdates) {
this.checkVersion();
}
if (this.config.deleteAll && !this.config.yes) {
this.showDeleteAllWarning();
this.uiWarning.confirm$
.pipe(
tap(() => {
this.activeComponent = this.uiResults;
this.uiWarning.setDeleteAllWarningVisibility(false);
this.uiService.renderAll();
this.scan();
}),
)
.subscribe();
return;
}
this.scan();
}
private showDeleteAllWarning(): void {
this.uiWarning.setDeleteAllWarningVisibility(true);
this.activeComponent = this.uiWarning;
}
private initUi(): void {
this.uiHeader = new HeaderUi(this.config);
this.uiService.add(this.uiHeader);
this.uiResults = new ResultsUi(this.resultsService, this.consoleService);
this.uiService.add(this.uiResults);
this.uiStats = new StatsUi(this.config, this.resultsService, this.logger);
this.uiService.add(this.uiStats);
this.uiStatus = new StatusUi(this.spinnerService, this.searchStatus);
this.uiService.add(this.uiStatus);
this.uiGeneral = new GeneralUi();
this.uiService.add(this.uiGeneral);
this.uiLogs = new LogsUi(this.logger);
this.uiService.add(this.uiLogs);
this.uiWarning = new WarningUi();
this.uiService.add(this.uiWarning);
// Set Events
this.uiResults.delete$.subscribe((folder) => this.deleteFolder(folder));
this.uiResults.showErrors$.subscribe(() => this.showErrorPopup(true));
this.uiLogs.close$.subscribe(() => this.showErrorPopup(false));
this.uiResults.openFolder$.subscribe((path) => openExplorer(path));
this.uiResults.showDetails$.subscribe((folder) =>
this.openResultsDetails(folder),
);
this.uiResults.endNpkill$.subscribe(() => this.quit());
this.uiResults.goOptions$.subscribe(() => this.openOptions());
this.uiResults.search$.subscribe((state) => {
if (state === null) {
this.uiHeader.setSearch(null);
} else {
this.uiHeader.setSearch(state.text, state.isInputActive);
}
});
// Activate the main interactive component
this.activeComponent = this.uiResults;
}
private openOptions(): void {
const changeConfig$ = new Subject<Partial<IConfig>>();
const optionsUi = new OptionsUi(changeConfig$, this.config);
this.uiResults.clear();
this.uiResults.setVisible(false);
this.uiService.add(optionsUi);
this.activeComponent = optionsUi;
this.uiHeader.menuIndex$.next(MENU_BAR_OPTIONS.OPTIONS);
this.uiStats.reset();
this.uiService.renderAll();
changeConfig$.subscribe((configChanges) => {
Object.assign(this.config, configChanges);
if (
configChanges.targets ||
configChanges.folderRoot ||
Object.keys(configChanges).includes('excludeSensitiveResults') ||
configChanges.exclude
) {
this.scan();
}
if (configChanges.sortBy) {
this.resultsService.sortResults(configChanges.sortBy);
}
if (configChanges.sizeUnit) {
this.resultsService.setSizeUnit(configChanges.sizeUnit);
}
this.logger.info(`Config updated: ${JSON.stringify(configChanges)}`);
this.uiService.renderAll();
});
optionsUi.goToHelp$.subscribe(() => {
const helpUi = new HelpUi();
this.uiService.add(helpUi);
this.activeComponent = helpUi;
optionsUi.clear();
optionsUi.setVisible(false);
this.uiHeader.menuIndex$.next(MENU_BAR_OPTIONS.HELP);
this.uiStats.reset();
this.uiService.renderAll();
helpUi.render();
helpUi.goToOptions$.subscribe(() => {
helpUi.clear();
this.activeComponent = optionsUi;
this.uiService.remove(helpUi.id);
optionsUi.clear();
optionsUi.setVisible(true);
this.uiHeader.menuIndex$.next(MENU_BAR_OPTIONS.OPTIONS);
this.uiStats.reset();
this.uiService.renderAll();
});
});
optionsUi.goBack$.subscribe(() => {
optionsUi.clear();
this.activeComponent = this.uiResults;
this.uiService.remove(optionsUi.id);
this.uiResults.clear();
this.uiResults.setVisible(true);
this.uiHeader.menuIndex$.next(MENU_BAR_OPTIONS.DELETE);
this.uiStats.reset();
this.uiService.renderAll();
});
}
private openResultsDetails(folder: CliScanFoundFolder): void {
const detailsUi = new ResultDetailsUi(folder, this.config);
this.uiResults.clear();
this.uiResults.setVisible(false);
this.uiService.add(detailsUi);
this.activeComponent = detailsUi;
// detailsUi.render();
this.uiHeader.menuIndex$.next(MENU_BAR_OPTIONS.INFO);
this.uiStats.reset();
this.uiService.renderAll();
detailsUi.openFolder$.subscribe((path) => openExplorer(path));
detailsUi.goBack$.subscribe(() => {
detailsUi.clear();
this.activeComponent = this.uiResults;
this.uiService.remove(detailsUi.id);
this.uiResults.clear();
this.uiResults.setVisible(true);
this.uiHeader.menuIndex$.next(MENU_BAR_OPTIONS.DELETE);
this.uiStats.reset();
this.uiService.renderAll();
});
}
private loadConfigFile(): void {
const configPathArg = process.argv.indexOf('--config');
const customConfigPath =
configPathArg !== -1 ? process.argv[configPathArg + 1] : undefined;
const result = this.configService.loadConfig(customConfigPath);
if (result.error) {
const isDefaultLocationNotFound =
!customConfigPath && result.error === undefined;
if (isDefaultLocationNotFound) {
this.logger.info(`No config file found at ${result.configPath}`);
} else {
this.logger.error(`Configuration error: ${result.error}`);
console.log(
`${pc.red(pc.bold('Configuration Error'))} (${pc.yellow(result.configPath)})`,
);
console.log(pc.red(`${result.error}\n`));
console.log(
pc.gray(
'Please fix the configuration file and try again.\n' +
'For configuration reference, see: https://npkill.js.org/docs/npkillrc',
),
);
this.exitWithError();
}
}
if (!result.config) {
return;
}
this.logger.info(`Loaded config from ${result.configPath}`);
if (result.config.rootDir !== undefined) {
this.config.folderRoot = result.config.rootDir;
}
if (result.config.exclude !== undefined) {
this.config.exclude = [
...new Set([...this.config.exclude, ...result.config.exclude]),
];
}
if (result.config.sortBy !== undefined) {
this.config.sortBy = result.config.sortBy;
}
if (result.config.sizeUnit !== undefined) {
this.config.sizeUnit = result.config.sizeUnit;
}
if (result.config.excludeSensitiveResults !== undefined) {
this.config.excludeSensitiveResults =
result.config.excludeSensitiveResults;
}
if (result.config.dryRun !== undefined) {
this.config.dryRun = result.config.dryRun;
}
if (result.config.checkUpdates !== undefined) {
this.config.checkUpdates = result.config.checkUpdates;
}
const userProfiles = this.configService.getUserDefinedProfiles(
result.config,
);
const profileCount = Object.keys(userProfiles).length;
if (profileCount > 0) {
this.profilesService.setUserDefinedProfiles(userProfiles);
this.logger.info(`Loaded ${profileCount} custom profile(s) from config`);
}
if (result.config.defaultProfiles !== undefined) {
// Store default profiles from config to be used later if no CLI profiles are specified
this.config.profiles = result.config.defaultProfiles;
this.logger.info(
`Default profiles set from config: ${result.config.defaultProfiles.join(', ')}`,
);
}
}
private parseArguments(): void {
const options = this.consoleService.getParameters(process.argv);
if (options.isTrue('help')) {
this.showHelp();
this.exitGracefully();
}
if (options.isTrue('version')) {
this.showProgramVersion();
this.exitGracefully();
}
if (options.isTrue('profiles') && options.isTrue('target-folders')) {
console.log('Cannot use both --profiles and --targets options together.');
this.exitGracefully();
}
if (
options.isTrue('profiles') &&
options.getStrings('profiles').length === 0
) {
console.log(pc.bold(pc.bgYellow(pc.black(' Available profiles '))));
console.log(
`Remember: ${pc.bold(pc.yellow('context matters'))}. What's safe to remove in one project or ecosystem could be important in another.\n`,
);
const defaultProfiles =
this.config.profiles.length > 0
? this.config.profiles
: [DEFAULT_PROFILE];
const userProfiles = this.profilesService.getProfiles('user');
const baseProfiles = this.profilesService.getProfiles('base');
let profilesToPrint = '';
if (Object.keys(userProfiles).length > 0) {
profilesToPrint += Object.entries(userProfiles).reduce(
(acc, [name, profile]) => {
const isDefault = defaultProfiles.includes(name);
const entry =
` ${pc.green(name)}${isDefault ? pc.italic(pc.magenta(' (default)')) : ''} ${pc.cyan('(user-defined)')} - ${profile.description}\n` +
pc.gray(` ${profile.targets.join(pc.italic(','))}\n\n`);
return acc + entry;
},
'',
);
}
profilesToPrint += Object.entries(baseProfiles).reduce(
(acc, [name, profile]) => {
const isDefault = defaultProfiles.includes(name);
const entry =
` ${pc.green(name)}${isDefault ? pc.italic(pc.magenta(' (default)')) : ''} - ${profile.description}\n` +
pc.gray(` ${profile.targets.join(pc.italic(','))}\n\n`);
return acc + entry;
},
'',
);
console.log(profilesToPrint);
this.exitGracefully();
}
if (options.isTrue('delete-all')) {
if (!options.isTrue('target-folders') || options.isTrue('profiles')) {
// TODO mejorar mensaje e incluir tip buscar lista targets de un profile.
console.log('--delete-all only can be used with --targets.');
console.log(
'You can copy all targets from a profile with `npkill --profiles`.',
);
this.exitWithError();
}
this.config.deleteAll = true;
}
if (options.isTrue('sort-by')) {
if (!this.isValidSortParam(options.getString('sort-by'))) {
this.invalidSortParam();
}
this.config.sortBy = options.getString('sort-by');
}
const exclude = options.getString('exclude');
if (exclude !== undefined && exclude !== '') {
console.log('EXCLUDE', exclude);
const userExcludeList = this.consoleService
.splitData(this.consoleService.replaceString(exclude, '"', ''), ',')
.map((path) => path.trim())
.filter(Boolean)
.map(path.normalize);
// Add custom filters to the default exclude list.
this.config.exclude = [...this.config.exclude, ...userExcludeList];
}
// Set folder root: CLI --directory takes precedence, then config rootDir, then process.cwd()
if (options.isTrue('directory')) {
this.config.folderRoot = options.getString('directory');
} else if (!this.config.folderRoot) {
// Only use process.cwd() if folderRoot wasn't set by config
this.config.folderRoot = process.cwd();
}
if (options.isTrue('full-scan')) {
this.config.folderRoot = homedir();
}
if (options.isTrue('hide-errors')) {
this.config.showErrors = false;
}
if (options.isTrue('size-unit')) {
const sizeUnit = options.getString('size-unit');
if (this.isValidSizeUnit(sizeUnit)) {
this.config.sizeUnit = sizeUnit as 'auto' | 'mb' | 'gb';
} else {
this.invalidSizeUnitParam();
return;
}
}
if (options.isTrue('no-check-updates')) {
this.config.checkUpdates = false;
}
if (!options.isTrue('target-folders')) {
if (!options.isTrue('profiles')) {
// Use defaultProfiles from config if available, otherwise use DEFAULT_PROFILE
const profilesToUse =
this.config.profiles.length > 0
? this.config.profiles
: [DEFAULT_PROFILE];
this.logger.info(
`Using default profile targets (${profilesToUse.join(', ')})`,
);
this.config.targets =
this.profilesService.getTargetsFromProfiles(profilesToUse);
} else {
const selectedProfiles = options.getStrings('profiles');
const badProfiles =
this.profilesService.getInvalidProfileNames(selectedProfiles);
if (badProfiles.length > 0) {
this.logger.warn(
`The following profiles are invalid: ${badProfiles.join(', ')}`,
);
const profileText = badProfiles.length > 1 ? 'profiles' : 'profile';
console.log(pc.bold(pc.bgRed(pc.white(` Invalid ${profileText} `))));
console.log(
`The following ${profileText} are invalid: ${pc.red(badProfiles.join(', '))}.`,
);
console.log(
`You can list the available profiles with ${pc.bold(pc.green('--profiles'))} command ${pc.gray('(without arguments)')}.`,
);
this.exitWithError();
}
const targets =
this.profilesService.getTargetsFromProfiles(selectedProfiles);
this.logger.info(
`Using profiles ${selectedProfiles.join(', ')} | With targets ${targets.join(', ')}`,
);
this.config.profiles = selectedProfiles;
this.config.targets = targets;
}
}
if (options.isTrue('target-folders')) {
this.config.targets = options.getString('target-folders').split(',');
this.config.profiles = [];
}
if (options.isTrue('exclude-sensitive')) {
this.config.excludeSensitiveResults = true;
}
if (options.isTrue('dry-run')) {
this.config.dryRun = true;
}
if (options.isTrue('yes')) {
this.config.yes = true;
}
if (options.isTrue('jsonStream')) {
this.config.jsonStream = true;
}
if (options.isTrue('jsonSimple')) {
this.config.jsonSimple = true;
}
if (this.config.jsonStream && this.config.jsonSimple) {
this.logger.error(ERROR_MSG.CANT_USE_BOTH_JSON_OPTIONS);
this.exitWithError();
}
// Remove trailing slash from folderRoot for consistency
this.config.folderRoot = this.config.folderRoot.replace(/[/\\]$/, '');
}
private showErrorPopup(visible: boolean): void {
this.uiLogs.setVisible(visible);
// Need convert to pattern and have a stack for recover latest
// component.
this.uiResults.freezed = visible;
this.uiStats.freezed = visible;
this.uiStatus.freezed = visible;
if (visible) {
this.activeComponent = this.uiLogs;
this.uiLogs.render();
} else {
this.activeComponent = this.uiResults;
this.uiService.renderAll();
}
}
private invalidSortParam(): void {
this.uiService.print(INFO_MSGS.NO_VALID_SORT_NAME);
this.logger.error(INFO_MSGS.NO_VALID_SORT_NAME);
this.exitWithError();
}
private showHelp(): void {
new HelpCommandUi(this.consoleService).show();
}
private showProgramVersion(): void {
this.uiService.print('v' + this.getVersion());
}
private isValidColor(color: string): boolean {
return Object.keys(COLORS).some((validColor) => validColor === color);
}
private isValidSortParam(sortName: string): boolean {
return Object.keys(FOLDER_SORT).includes(sortName);
}
private isValidSizeUnit(sizeUnit: string): boolean {
return ['auto', 'mb', 'gb'].includes(sizeUnit);
}
private invalidSizeUnitParam(): void {
this.uiService.print(INFO_MSGS.NO_VALID_SIZE_UNIT);
this.logger.error(INFO_MSGS.NO_VALID_SIZE_UNIT);
this.exitWithError();
}
private getVersion(): string {
const packageJson = _dirname + '/../package.json';
const packageData = JSON.parse(getFileContent(packageJson));
return packageData.version;
}
private prepareScreen(): void {
this.uiService.setRawMode();
// this.uiService.prepareUi();
this.uiService.setCursorVisible(false);
this.uiService.clear();
this.uiService.renderAll();
}
private checkRequirements(): void {
this.checkScreenRequirements();
this.checkFileRequirements();
}
private checkScreenRequirements(): void {
if (this.isTerminalTooSmall()) {
this.uiService.print(INFO_MSGS.MIN_CLI_CLOMUNS);
this.logger.error(INFO_MSGS.MIN_CLI_CLOMUNS);
this.exitWithError();
}
}
private checkFileRequirements(): void {
const result = this.npkill.isValidRootFolder(this.config.folderRoot);
if (!result.isValid) {
const errorMessage =
result.invalidReason || 'Root folder is not valid. Unknown reason';
this.uiService.print(errorMessage);
this.logger.error(errorMessage);
this.exitWithError();
}
}
private checkVersion(): void {
this.logger.info('Checking updates...');
this.updateService
.isUpdated(this.getVersion())
.then((isUpdated: boolean) => {
if (!isUpdated) {
this.showUpdateMessage();
this.logger.info('New version found!');
} else {
this.logger.info('Npkill is update');
}
return isUpdated;
})
.catch((err: Error) => {
const errorMessage =
ERROR_MSG.CANT_GET_REMOTE_VERSION + ': ' + err.message;
this.newError(errorMessage);
});
}
private showUpdateMessage(): void {
const message = pc.magenta(INFO_MSGS.NEW_UPDATE_FOUND);
this.uiService.printAt(message, UI_POSITIONS.NEW_UPDATE_FOUND);
}
private isTerminalTooSmall(): boolean {
return this.stdout.columns <= MIN_CLI_COLUMNS_SIZE;
}
private printFoldersSection(): void {
this.uiResults.render();
}
private setupEventsListener(): void {
this.uiService.stdin.on('keypress', (_, key: IKeyPress) => {
if (key['name'] !== '') {
this.keyPress(key);
} else {
throw new Error('Invalid key: ' + JSON.stringify(key));
}
});
this.stdout.on('resize', () => {
this.uiService.clear();
this.uiService.renderAll();
});
process.on('uncaughtException', (error: Error) => {
this.newError(error.message);
});
process.on('unhandledRejection', (error: Error) => {
this.newError(error.stack ?? error.message);
});
}
private keyPress(key: IKeyPress): void {
const { name, ctrl } = key;
if (this.isQuitKey(ctrl, name)) {
this.quit();
}
if (this.activeComponent === null) {
this.logger.error('activeComponent is NULL in Controller.');
return;
}
this.activeComponent.onKeyInput(key);
}
private scan(): void {
this.initializeScan();
const shouldOutputInJson = this.config.jsonSimple || this.config.jsonStream;
if (shouldOutputInJson) {
this.scanInJson();
} else {
this.scanInUiMode();
}
}
private initializeScan(): void {
this.searchStatus.reset();
this.resultsService.reset();
this.resultsService.setSizeUnit(this.config.sizeUnit);
}
private scanInJson(): void {
const isStreamMode = this.config.jsonStream;
this.jsonOutputService.initializeSession(isStreamMode);
this.scanService
.scan(this.config)
.pipe(
mergeMap(
(nodeFolder) =>
this.scanService.calculateFolderStats(nodeFolder, {
getModificationTimeForSensitiveResults: true,
}),
10, // Limit to 10 concurrent stat calculations at a time
),
tap((folder) => this.jsonOutputService.processResult(folder)),
)
.subscribe({
error: (error) => this.jsonOutputService.writeError(error),
complete: () => {
this.jsonOutputService.completeScan();
this.exitGracefully();
},
});
}
private scanSubscription: any = null;
private scanInUiMode(): void {
if (this.scanSubscription) {
this.scanSubscription.unsubscribe();
}
this.uiStatus.reset();
this.uiStatus.start();
this.searchStart = Date.now();
this.scanSubscription = this.scanService
.scan(this.config)
.pipe(
tap((nodeFolder) => this.processNodeFolderForUi(nodeFolder)),
mergeMap(
(nodeFolder) => this.scanService.calculateFolderStats(nodeFolder),
10, // Limit to 10 concurrent stat calculations at a time
),
tap((folder) => this.processFolderStatsForUi(folder)),
)
.subscribe({
next: () => this.printFoldersSection(),
error: (error) => this.newError(error),
complete: () => this.completeSearch(),
});
}
private setupJsonModeSignalHandlers(): void {
const gracefulShutdown = () => {
this.jsonOutputService.handleShutdown();
this.exitGracefully();
};
process.on('SIGINT', gracefulShutdown);
process.on('SIGTERM', gracefulShutdown);
}
private processNodeFolderForUi(nodeFolder: CliScanFoundFolder): void {
this.searchStatus.newResult();
this.resultsService.addResult(nodeFolder);
if (this.config.sortBy === 'path') {
this.resultsService.sortResults(this.config.sortBy);
this.uiResults.clear();
}
this.uiResults.render();
}
private processFolderStatsForUi(folder: CliScanFoundFolder): void {
this.searchStatus.completeStatCalculation();
this.finishFolderStats();
if (this.config.deleteAll) {
this.deleteFolder(folder);
}
}
private finishFolderStats(): void {
const needSort =
this.config.sortBy === 'size' || this.config.sortBy === 'age';
if (needSort) {
this.resultsService.sortResults(this.config.sortBy);
this.uiResults.clear();
}
this.uiStats.render();
this.printFoldersSection();
}
private completeSearch(): void {
this.setSearchDuration();
this.uiResults.completeSearch();
this.uiStatus.completeSearch(this.searchDuration);
}
private setSearchDuration(): void {
this.searchDuration = +((Date.now() - this.searchStart) / 1000).toFixed(2);
}
private isQuitKey(ctrl: boolean, name: string): boolean {
return ctrl && name === 'c';
}
private exitWithError(): void {
this.resetConsoleState();
const logPath = this.logger.getSuggestLogFilePath();
this.logger.saveToFile(logPath);
// eslint-disable-next-line n/no-process-exit
process.exit(1);
}
private exitGracefully(): void {
this.resetConsoleState();
const logPath = this.logger.getSuggestLogFilePath();
this.logger.saveToFile(logPath);
// eslint-disable-next-line n/no-process-exit
process.exit(0);
}
private quit(): void {
this.uiService.setRawMode(false);
this.uiService.clear();
this.uiService.setCursorVisible(true);
this.printExitMessage();
this.logger.info('Thank for using npkill. Bye!');
const logPath = this.logger.getSuggestLogFilePath();
this.logger.saveToFile(logPath);
// eslint-disable-next-line n/no-process-exit
process.exit(0);
}
private resetConsoleState(): void {
this.uiService.print('\n');
this.uiService.setRawMode(false);
this.uiService.setCursorVisible(true);
}
private printExitMessage(): void {
const { spaceReleased } = this.resultsService.getStats();
new GeneralUi().printExitMessage({ spaceReleased });
}
private deleteFolder(folder: CliScanFoundFolder): void {
if (folder.status === 'deleted' || folder.status === 'deleting') {
return;
}
if (!isSafeToDelete(folder.path, this.config.targets)) {
this.newError(`Folder not safe to delete: ${String(folder.path)}`);
return;
}
folder.status = 'deleting';
this.searchStatus.pendingDeletions++;
this.uiStatus.render();
this.printFoldersSection();
firstValueFrom(
this.npkill.delete$(String(folder.path), { dryRun: this.config.dryRun }),
)
.then(() => {
folder.status = 'deleted';
this.searchStatus.pendingDeletions--;
this.uiStats.render();
this.uiStatus.render();
this.printFoldersSection();
return folder;
})
.catch((e) => {
folder.status = 'error-deleting';
this.searchStatus.pendingDeletions--;
this.uiStatus.render();
this.printFoldersSection();
this.newError(e.message);
});
}
private newError(error: string): void {
this.logger.error(error);
this.uiStats.render();
}
}
================================================
FILE: src/cli/interfaces/cli-options.interface.ts
================================================
export interface ICliOptions {
arg: string[];
name: string;
description: string;
}
================================================
FILE: src/cli/interfaces/command-keys.interface.ts
================================================
export interface IKeysCommand {
up: () => void;
down: () => void;
space: () => void;
j: () => void;
k: () => void;
h: () => void;
l: () => void;
d: () => void;
u: () => void;
pageup: () => void;
pagedown: () => void;
home: () => void;
end: () => void;
e: () => void;
execute: (command: string, params?: string[]) => number;
}
================================================
FILE: src/cli/interfaces/config.interface.ts
================================================
export interface IConfig {
profiles: string[];
folderRoot: string;
checkUpdates: boolean;
deleteAll: boolean;
sizeUnit: 'auto' | 'mb' | 'gb';
maxSimultaneousSearch: number;
showErrors: boolean;
sortBy: string;
targets: string[];
exclude: string[];
excludeSensitiveResults: boolean;
dryRun: boolean;
yes: boolean;
jsonStream: boolean;
jsonSimple: boolean;
}
================================================
FILE: src/cli/interfaces/index.ts
================================================
export * from './cli-options.interface.js';
export * from './command-keys.interface.js';
export * from './config.interface.js';
export * from '@core/interfaces/file-service.interface.js';
export * from '@core/interfaces/folder.interface.js';
export * from './key-press.interface.js';
export * from './stats.interface.js';
export * from './ui-positions.interface.js';
export * from './version.interface.js';
export * from './node-version.interface.js';
export * from './json-output.interface.js';
================================================
FILE: src/cli/interfaces/json-output.interface.ts
================================================
import { CliScanFoundFolder } from './stats.interface.js';
export interface JsonOutputBase {
version: number;
}
export interface JsonCliScanFoundFolder
extends Omit<CliScanFoundFolder, 'status'> {}
/**
* JSON output format for streaming mode (--json-stream).
* Each result is output as a separate JSON object on its own line.
*/
export interface JsonStreamOutput extends JsonOutputBase {
result: JsonCliScanFoundFolder;
}
/**
* JSON output format for simple mode (--json).
* All results are collected and output as a single JSON object at the end.
*/
export interface JsonSimpleOutput extends JsonOutputBase {
results: JsonCliScanFoundFolder[];
meta: {
resultsCount: number;
/** Scan duration in milliseconds */
runDuration: number;
};
}
export interface JsonErrorOutput extends JsonOutputBase {
error: true;
message: string;
timestamp: number; // Unix timestamp in milliseconds
}
================================================
FILE: src/cli/interfaces/key-press.interface.ts
================================================
export interface IKeyPress {
name: string;
meta: boolean;
ctrl: boolean;
shift: boolean;
sequence: string;
}
================================================
FILE: src/cli/interfaces/node-version.interface.ts
================================================
export interface INodeVersion {
major: number;
minor: number;
patch: number;
}
================================================
FILE: src/cli/interfaces/stats.interface.ts
================================================
import { ScanFoundFolder } from '../../core/interfaces/index.js';
export interface CliScanFoundFolder extends ScanFoundFolder {
size: number;
modificationTime: number;
status: 'live' | 'deleting' | 'error-deleting' | 'deleted';
}
export interface IResultTypeCount {
type: string;
count: number;
}
export interface IStats {
spaceReleased: string;
totalSpace: string;
resultsTypesCount: IResultTypeCount[];
}
================================================
FILE: src/cli/interfaces/ui-positions.interface.ts
================================================
export interface IPosition {
x: number;
y: number;
}
export type IUiPosition = Record<string, IPosition>;
================================================
FILE: src/cli/interfaces/version.interface.ts
================================================
export interface IVersion {
major: number;
minor: number;
patch: number;
}
================================================
FILE: src/cli/models/start-parameters.model.ts
================================================
export class StartParameters {
private values: Record<string, string | boolean> = {};
add(key: string, value: string | boolean): void {
this.values[key] = value;
}
isTrue(key: string): boolean {
const value = this.values[key];
return value !== undefined && (value === true || value !== 'false');
}
getString(key: string): string {
const value = this.values[key];
if (typeof value === 'boolean') {
return value.toString();
}
return value;
}
getStrings(key: string): string[] {
const value = this.values[key];
if (!value || typeof value === 'boolean') {
return [];
}
return value.split(',').map((item) => item.trim());
}
}
================================================
FILE: src/cli/services/console.service.ts
================================================
import { OPTIONS, WIDTH_OVERFLOW } from '../../constants/index.js';
import { ICliOptions } from '../interfaces/cli-options.interface.js';
import { extname } from 'path';
import * as readline from 'node:readline';
import { StartParameters } from '../models/start-parameters.model.js';
export class ConsoleService {
getParameters(rawArgv: string[]): StartParameters {
// This needs a refactor, but fck, is a urgent update
const rawProgramArgvs = this.removeSystemArgvs(rawArgv);
const argvs = this.normalizeParams(rawProgramArgvs);
const options: StartParameters = new StartParameters();
argvs.forEach((argv, index) => {
if (!this.isArgOption(argv) || !this.isValidOption(argv)) {
return;
}
const nextArgv = argvs[index + 1];
const option = this.getOption(argv);
if (option === undefined) {
throw new Error('Invalid option name.');
}
const optionName = option.name;
options.add(
optionName,
this.isArgHavingParams(nextArgv) ? nextArgv : true,
);
});
return options;
}
splitWordsByWidth(text: string, width: number): string[] {
const splitRegex = new RegExp(
`(?![^\\n]{1,${width}}$)([^\\n]{1,${width}})\\s`,
'g',
);
const splitText = this.replaceString(text, splitRegex, '$1\n');
return this.splitData(splitText);
}
splitData(data: string, separator = '\n'): string[] {
if (data === '') {
return [];
}
return data.split(separator);
}
replaceString(
text: string,
textToReplace: string | RegExp,
replaceValue: string,
): string {
return text.replace(textToReplace, replaceValue);
}
shortenText(text: string, width: number, startCut = 0): string {
if (!this.isValidShortenParams(text, width, startCut)) {
return text;
}
const startPartB = text.length - (width - startCut - WIDTH_OVERFLOW.length);
const partA = text.substring(startCut, -1);
const partB = text.substring(startPartB, text.length);
return partA + WIDTH_OVERFLOW + partB;
}
isRunningBuild(): boolean {
return extname(import.meta.url) === '.js';
}
startListenKeyEvents(): void {
readline.emitKeypressEvents(process.stdin);
}
/** Argvs can be specified for example by
* "--sort size" and "--sort=size". The main function
* expect the parameters as the first form so this
* method convert the second to first.
*/
private normalizeParams(argvs: string[]): string[] {
return argvs.join('=').split('=');
}
private isValidShortenParams(
text: string,
width: number,
startCut: number,
): boolean {
return (
startCut <= width &&
text.length >= width &&
!this.isNegative(width) &&
!this.isNegative(startCut)
);
}
private removeSystemArgvs(allArgv: string[]): string[] {
return allArgv.slice(2);
}
private isArgOption(argv: string): boolean {
return argv.charAt(0) === '-';
}
private isArgHavingParams(nextArgv: string): boolean {
return (
nextArgv !== undefined && nextArgv !== '' && !this.isArgOption(nextArgv)
);
}
private isValidOption(arg: string): boolean {
return OPTIONS.some((option) => option.arg.includes(arg));
}
private getOption(arg: string): ICliOptions | undefined {
return OPTIONS.find((option) => option.arg.includes(arg));
}
private isNegative(numb: number): boolean {
return numb < 0;
}
}
================================================
FILE: src/cli/services/https.service.ts
================================================
import * as https from 'node:https';
export class HttpsService {
async getJson(url: string): Promise<Record<string, string>> {
return new Promise((resolve, reject) => {
const fail = (err: Error): void => {
reject(err);
};
const request = https.get(url, (res) => {
if (!this.isCorrectResponse(res.statusCode ?? -1)) {
const error = new Error(res.statusMessage ?? 'Unknown error');
fail(error);
return;
}
res.setEncoding('utf8');
let body = '';
res.on('data', (data: string) => {
body += data;
});
res.on('end', () => {
resolve(JSON.parse(body));
});
});
request.on('error', (error) => fail(error));
});
}
private isCorrectResponse(statusCode: number): boolean {
const correctRangeStart = 200;
const correctRangeEnd = 299;
return statusCode >= correctRangeStart && statusCode <= correctRangeEnd;
}
}
================================================
FILE: src/cli/services/index.ts
================================================
export * from './console.service.js';
export * from './https.service.js';
export * from './results.service.js';
export * from './spinner.service.js';
export * from './update.service.js';
export * from './json-output.service.js';
export * from '../../core/services/stream.service.js';
================================================
FILE: src/cli/services/json-output.service.ts
================================================
import { CliScanFoundFolder } from '../interfaces/stats.interface.js';
import {
JsonStreamOutput,
JsonSimpleOutput,
JsonErrorOutput,
JsonCliScanFoundFolder,
} from '../interfaces/json-output.interface.js';
import { convertGbToBytes } from '../../utils/unit-conversions.js';
export class JsonOutputService {
private readonly OUTPUT_VERSION = 1;
private results: JsonCliScanFoundFolder[] = [];
private scanStartTime: number = 0;
private isStreamMode: boolean = false;
constructor(
private readonly stdout: NodeJS.WriteStream = process.stdout,
private readonly stderr: NodeJS.WriteStream = process.stderr,
) {}
initializeSession(streamMode: boolean = false): void {
this.results = [];
this.scanStartTime = Date.now();
this.isStreamMode = streamMode;
}
processResult(folder: CliScanFoundFolder): void {
if (this.isStreamMode) {
this.writeStreamResult(folder);
} else {
this.addResult(folder);
}
}
completeScan(): void {
if (!this.isStreamMode && this.results.length > 0) {
this.writeSimpleResults();
}
}
private writeStreamResult(folder: CliScanFoundFolder): void {
const output: JsonStreamOutput = {
version: this.OUTPUT_VERSION,
result: this.sanitizeFolderForOutput(folder),
};
try {
this.stdout.write(JSON.stringify(output) + '\n');
} catch (error) {
const errorMessage =
error instanceof Error
? error.message
: 'Unknown JSON serialization error';
this.writeError(`Failed to serialize result to JSON: ${errorMessage}`);
}
}
private addResult(folder: CliScanFoundFolder): void {
this.results.push(this.sanitizeFolderForOutput(folder));
}
private writeSimpleResults(): void {
const runDuration = Date.now() - this.scanStartTime;
const output: JsonSimpleOutput = {
version: this.OUTPUT_VERSION,
results: this.results,
meta: {
resultsCount: this.results.length,
runDuration,
},
};
try {
this.stdout.write(JSON.stringify(output, null, 2) + '\n');
} catch (error) {
const errorMessage =
error instanceof Error
? error.message
: 'Unknown JSON serialization error';
this.writeError(`Failed to serialize results to JSON: ${errorMessage}`);
}
}
writeError(error: Error | string): void {
const errorMessage = error instanceof Error ? error.message : error;
const errorOutput: JsonErrorOutput = {
version: this.OUTPUT_VERSION,
error: true,
message: errorMessage,
timestamp: new Date().getDate(),
};
this.stderr.write(JSON.stringify(errorOutput) + '\n');
}
getResultsCount(): number {
return this.results.length;
}
handleShutdown(): void {
if (!this.isStreamMode && this.results.length > 0) {
this.writeSimpleResults();
}
}
private sanitizeFolderForOutput(
folder: CliScanFoundFolder,
): JsonCliScanFoundFolder {
return {
path: folder.path,
size: convertGbToBytes(folder.size),
modificationTime: folder.modificationTime,
riskAnalysis: folder.riskAnalysis
? {
isSensitive: folder.riskAnalysis.isSensitive,
reason: folder.riskAnalysis.reason,
}
: undefined,
};
}
}
================================================
FILE: src/cli/services/results.service.ts
================================================
import {
CliScanFoundFolder,
IStats,
IResultTypeCount,
} from '../interfaces/index.js';
import { FOLDER_SORT } from '../../constants/sort.result.js';
import { formatSize } from '../../utils/unit-conversions.js';
import path from 'path';
export class ResultsService {
results: CliScanFoundFolder[] = [];
private sizeUnit: 'auto' | 'mb' | 'gb' = 'auto';
addResult(result: CliScanFoundFolder): void {
this.results = [...this.results, result];
}
sortResults(method: string): void {
this.results = this.results.sort(FOLDER_SORT[method]);
}
reset(): void {
this.results = [];
}
setSizeUnit(sizeUnit: 'auto' | 'mb' | 'gb'): void {
this.sizeUnit = sizeUnit;
}
getStats(): IStats {
let spaceReleased = 0;
const typeCounts = new Map<string, number>();
const totalSpace = this.results.reduce((total, folder) => {
if (folder.status === 'deleted') {
spaceReleased += folder.size;
}
const folderType = path.basename(folder.path);
typeCounts.set(folderType, (typeCounts.get(folderType) || 0) + 1);
return total + folder.size;
}, 0);
const formattedTotal = formatSize(totalSpace, this.sizeUnit);
const formattedReleased = formatSize(spaceReleased, this.sizeUnit);
const resultsTypesCount: IResultTypeCount[] = Array.from(
typeCounts.entries(),
)
.map(([type, count]) => ({ type, count }))
.sort((a, b) => {
if (b.count !== a.count) {
return b.count - a.count;
}
return a.type.localeCompare(b.type);
});
return {
spaceReleased: formattedReleased.text,
totalSpace: formattedTotal.text,
resultsTypesCount,
};
}
}
================================================
FILE: src/cli/services/scan.service.ts
================================================
import { Npkill } from '@core/npkill';
import {
CliScanFoundFolder,
IConfig,
ScanFoundFolder,
ScanOptions,
SortBy,
} from '../interfaces';
import {
Observable,
filter,
firstValueFrom,
map,
switchMap,
tap,
catchError,
of,
timeout,
} from 'rxjs';
import { convertBytesToGb } from '../../utils/unit-conversions.js';
import { join } from 'path';
import os from 'os';
export interface CalculateFolderStatsOptions {
getModificationTimeForSensitiveResults: boolean;
}
export class ScanService {
constructor(private readonly npkill: Npkill) {}
scan(config: IConfig): Observable<CliScanFoundFolder> {
const { targets, exclude, sortBy } = config;
const params: ScanOptions = {
targets,
exclude,
performRiskAnalysis: true,
sortBy: sortBy as SortBy,
};
const results$ = this.npkill.startScan$(config.folderRoot, params);
const nonExcludedResults$ = results$.pipe(
filter(
(path) =>
!this.isExcludedDangerousDirectory(
path,
config.excludeSensitiveResults,
),
),
);
return nonExcludedResults$.pipe(
map<ScanFoundFolder, CliScanFoundFolder>(({ path, riskAnalysis }) => ({
path,
size: 0,
modificationTime: -1,
riskAnalysis,
status: 'live',
})),
);
}
calculateFolderStats(
nodeFolder: CliScanFoundFolder,
options: CalculateFolderStatsOptions = {
/** Saves resources by not scanning a result that is probably not of interest. */
getModificationTimeForSensitiveResults: false,
},
): Observable<CliScanFoundFolder> {
return this.npkill.getSize$(nodeFolder.path).pipe(
timeout(30000), // 30 seconds timeout
catchError(() => {
// If size calculation fails or times out, keep size as 0 but mark as calculated
nodeFolder.size = 0;
nodeFolder.modificationTime = 1; // 1 = calculated, -1 = not calculated
return of({ size: 0, unit: 'bytes' as const });
}),
tap(({ size }) => {
nodeFolder.size = convertBytesToGb(size);
}),
switchMap(async () => {
if (
nodeFolder.riskAnalysis?.isSensitive &&
!options.getModificationTimeForSensitiveResults
) {
nodeFolder.modificationTime = -1;
return nodeFolder;
}
const parentFolder = join(nodeFolder.path, '../');
const normalizedParent = parentFolder.replace(/\\/g, '/').toLowerCase();
const normalizedHome = os.homedir().replace(/\\/g, '/').toLowerCase();
const isDirectChildOfHome =
normalizedHome && normalizedParent === normalizedHome;
// If it's directly under HOME, skip modification time calculation
if (isDirectChildOfHome) {
nodeFolder.modificationTime = -1;
return nodeFolder;
}
// For other folders, scan the parent folder for modification time
try {
const result = await firstValueFrom(
this.npkill.getNewestFile$(parentFolder).pipe(
timeout(10000), // 10 seconds timeout for modification time
catchError(() => of(null)),
),
);
nodeFolder.modificationTime = result ? result.timestamp : 1;
return nodeFolder;
} catch {
nodeFolder.modificationTime = 1;
return nodeFolder;
}
}),
catchError(() => {
// Final fallback: mark as calculated with default values
nodeFolder.modificationTime = 1;
if (nodeFolder.size === undefined || nodeFolder.size === null) {
nodeFolder.size = 0;
}
return of(nodeFolder);
}),
);
}
private isExcludedDangerousDirectory(
scanResult: ScanFoundFolder,
excludeSensitiveResults: boolean,
): boolean {
return Boolean(
excludeSensitiveResults && scanResult.riskAnalysis?.isSensitive,
);
}
}
================================================
FILE: src/cli/services/spinner.service.ts
================================================
export class SpinnerService {
private spinner: string[] = [];
private count = -1;
setSpinner(spinner: string[]): void {
this.spinner = spinner;
this.reset();
}
nextFrame(): string {
this.updateCount();
return this.spinner[this.count];
}
reset(): void {
this.count = -1;
}
private updateCount(): void {
if (this.isLastFrame()) {
this.count = 0;
} else {
++this.count;
}
}
private isLastFrame(): boolean {
return this.count === this.spinner.length - 1;
}
}
================================================
FILE: src/cli/services/ui.service.ts
================================================
import ansiEscapes from 'ansi-escapes';
import { Position, BaseUi } from '../ui/index.js';
export class UiService {
stdin: NodeJS.ReadStream = process.stdin;
// public stdout: NodeJS.WriteStream = process.stdout;
uiComponents: BaseUi[] = [];
setRawMode(set = true): void {
if (this.stdin.isTTY) {
this.stdin.setRawMode(set);
}
process.stdin.resume();
}
setCursorVisible(visible: boolean): void {
const instruction = visible
? ansiEscapes.cursorShow
: ansiEscapes.cursorHide;
this.print(instruction);
}
add(component: BaseUi): void {
this.uiComponents.push(component);
}
remove(baseUiId: string): void {
this.uiComponents = this.uiComponents.filter((c) => c.id !== baseUiId);
}
renderAll(): void {
this.clear();
this.uiComponents.forEach((component) => {
if (component.visible) {
component.render();
}
});
}
setFreezeAll(freeze: boolean): void {
this.uiComponents.forEach((component) => {
component.freezed = freeze;
});
}
setVisibleAll(visible: boolean): void {
this.uiComponents.forEach((component) => {
component.setVisible(visible);
});
}
clear(): void {
this.print(ansiEscapes.clearTerminal);
}
print(text: string): void {
process.stdout.write.bind(process.stdout)(text);
}
printAt(message: string, position: Position): void {
this.setCursorAt(position);
this.print(message);
}
setCursorAt({ x, y }: Position): void {
this.print(ansiEscapes.cursorTo(x, y));
}
clearLine(row: number): void {
this.printAt(ansiEscapes.eraseLine, { x: 0, y: row });
}
}
================================================
FILE: src/cli/services/update.service.ts
================================================
import {
VERSION_CHECK_DIRECTION,
VERSION_KEY,
} from '../../constants/update.constants.js';
import { HttpsService } from './https.service.js';
export class UpdateService {
constructor(private readonly httpsService: HttpsService) {}
/**
* Check if localVersion is greater or equal to remote version
* ignoring the pre-release tag. ex: 1.3.12 = 1.3.12-21
*/
async isUpdated(localVersion: string): Promise<boolean> {
const removePreReaseTag = (value: string): string => value.split('-')[0];
const localVersionPrepared = removePreReaseTag(localVersion);
const remoteVersion = await this.getRemoteVersion();
const remoteVersionPrepared = removePreReaseTag(remoteVersion);
return this.compareVersions(localVersionPrepared, remoteVersionPrepared);
}
private compareVersions(local: string, remote: string): boolean {
return (
this.isSameVersion(local, remote) ||
this.isLocalVersionGreater(local, remote)
);
}
private async getRemoteVersion(): Promise<string> {
const response = await this.httpsService.getJson(VERSION_CHECK_DIRECTION);
return response[VERSION_KEY];
}
private isSameVersion(version1: string, version2: string): boolean {
return version1 === version2;
}
/** Valid to compare versions up to 99999.99999.99999 */
private isLocalVersionGreater(local: string, remote: string): boolean {
const leadingZeros = (value: string): string =>
('00000' + value).substring(-5);
const localLeaded = +local.split('.').map(leadingZeros).join('');
const remoteLeaded = +remote.split('.').map(leadingZeros).join('');
return localLeaded >= remoteLeaded;
}
}
================================================
FILE: src/cli/ui/base.ui.ts
================================================
import { IKeyPress } from '../interfaces/index.js';
import ansiEscapes from 'ansi-escapes';
export interface Position {
x: number;
y: number;
}
export interface InteractiveUi {
onKeyInput: (key: IKeyPress) => void;
}
export abstract class BaseUi {
public readonly id = Math.random().toString(36).substring(2, 10);
public freezed = false;
protected _position: Position;
protected _visible = true;
private readonly stdout: NodeJS.WriteStream = process.stdout;
protected printAt(message: string, position: Position): void {
this.setCursorAt(position);
this.print(message);
}
protected setCursorAt({ x, y }: Position): void {
this.print(ansiEscapes.cursorTo(x, y));
}
protected print(text: string): void {
if (this.freezed) {
return;
}
process.stdout.write.bind(process.stdout)(text);
}
protected clearLine(row: number): void {
this.printAt(ansiEscapes.eraseLine, { x: 0, y: row });
}
setPosition(position: Position, renderOnSet = true): void {
this._position = position;
if (renderOnSet) {
this.render();
}
}
setVisible(visible: boolean, renderOnSet = true): void {
this._visible = visible;
if (renderOnSet) {
this.render();
}
}
get position(): Position {
return this._position;
}
get visible(): boolean {
return this._visible;
}
get terminal(): { columns: number; rows: number } {
return {
columns: this.stdout.columns,
rows: this.stdout.rows,
};
}
abstract render(): void;
}
================================================
FILE: src/cli/ui/components/general.ui.ts
================================================
// This class in only a intermediate for the refactor.
import { BaseUi } from '../base.ui.js';
import pc from 'picocolors';
export class GeneralUi extends BaseUi {
render(): void {}
printExitMessage(stats: { spaceReleased: string }): void {
const { spaceReleased } = stats;
let exitMessage = `Space saved: ${spaceReleased}\n`;
exitMessage += pc.dim(
'Thanks for using npkill!\nLike it? Give us a star http://github.com/voidcosmos/npkill\n',
);
this.print(exitMessage);
}
}
================================================
FILE: src/cli/ui/components/header/header-ui.constants.ts
================================================
export enum MENU_BAR_OPTIONS {
HELP = 0,
OPTIONS = 1,
DELETE = 2,
INFO = 3,
}
================================================
FILE: src/cli/ui/components/header/header.ui.ts
================================================
import { BehaviorSubject } from 'rxjs';
import {
BANNER,
UI_POSITIONS,
MENU_BAR,
INFO_MSGS,
} from '../../../../constants/index.js';
import { BaseUi } from '../../base.ui.js';
import pc from 'picocolors';
import { IConfig } from '../../../../cli/interfaces/config.interface.js';
import { MENU_BAR_OPTIONS } from './header-ui.constants.js';
export class HeaderUi extends BaseUi {
programVersion: string;
private activeMenuIndex = MENU_BAR_OPTIONS.DELETE;
private searchMode = false;
private searchText = '';
private isSearchInputActive = false;
readonly menuIndex$ = new BehaviorSubject<MENU_BAR_OPTIONS>(
MENU_BAR_OPTIONS.DELETE,
);
constructor(private readonly config: IConfig) {
super();
this.menuIndex$.subscribe((menuIndex) => {
this.activeMenuIndex = menuIndex;
this.render();
});
}
setSearch(text: string | null, isInputActive = false) {
if (text === null) {
this.searchMode = false;
this.searchText = '';
this.isSearchInputActive = false;
} else {
this.searchMode = true;
this.searchText = text;
this.isSearchInputActive = isInputActive;
}
this.render();
}
render(): void {
// banner and tutorial
this.printAt(BANNER, UI_POSITIONS.INITIAL);
this.renderHeader();
this.renderMenuBar();
if (this.programVersion !== undefined) {
this.printAt(pc.gray(this.programVersion), UI_POSITIONS.VERSION);
}
if (this.config.dryRun) {
this.printAt(
pc.black(pc.bgMagenta(` ${INFO_MSGS.DRY_RUN} `)),
UI_POSITIONS.DRY_RUN_NOTICE,
);
}
// Columns headers
if (this.activeMenuIndex === MENU_BAR_OPTIONS.DELETE) {
this.printAt(pc.bgYellow(pc.black(INFO_MSGS.HEADER_COLUMNS)), {
x: this.terminal.columns - INFO_MSGS.HEADER_COLUMNS.length - 2,
y: UI_POSITIONS.FOLDER_SIZE_HEADER.y,
});
}
// npkill stats
this.printAt(pc.gray(INFO_MSGS.TOTAL_SPACE), UI_POSITIONS.TOTAL_SPACE);
this.printAt(
pc.gray(INFO_MSGS.SPACE_RELEASED),
UI_POSITIONS.SPACE_RELEASED,
);
}
private renderHeader(): void {
const { columns } = this.terminal;
const spaceToFill = Math.max(0, columns - 2);
this.printAt(
pc.bgYellow(' '.repeat(spaceToFill)),
UI_POSITIONS.TUTORIAL_TIP,
);
}
private renderMenuBar(): void {
if (this.searchMode) {
let searchText = ` Search: ${this.searchText} `;
if (this.isSearchInputActive) {
searchText = ` Search: ${this.searchText}_ `;
this.printAt(pc.bgBlue(pc.white(searchText)), {
x: 2,
y: UI_POSITIONS.TUTORIAL_TIP.y,
});
} else {
this.printAt(pc.bgWhite(pc.black(searchText)), {
x: 2,
y: UI_POSITIONS.TUTORIAL_TIP.y,
});
}
return;
}
const options = Object.values(MENU_BAR);
let xStart = 2;
for (const option of options) {
const isActive = option === options[this.activeMenuIndex];
const colorFn = isActive
? (v: string) => pc.bgYellow(pc.black(pc.bold(pc.underline(v))))
: (v: string) => pc.bgYellow(pc.gray(v));
this.printAt(colorFn(option), {
x: xStart,
y: UI_POSITIONS.TUTORIAL_TIP.y,
});
const MARGIN = 1;
xStart += option.length + MARGIN;
}
}
}
================================================
FILE: src/cli/ui/components/header/stats.ui.ts
================================================
import { UI_POSITIONS, INFO_MSGS } from '../../../../constants/index.js';
import { BaseUi } from '../../base.ui.js';
import { ResultsService } from '../../../services/results.service.js';
import { LoggerService } from '@core/services/logger.service.js';
import pc from 'picocolors';
import { IConfig } from '../../../interfaces/config.interface.js';
import { IPosition } from '../../../interfaces/ui-positions.interface.js';
interface ShowStatProps {
description: string;
value: string;
lastValueKey: 'totalSpace' | 'spaceReleased';
position: IPosition;
updateColor: 'green' | 'yellow';
}
type ResultTypeRowKey = 'row1' | 'row2' | 'row3' | 'row4' | 'row5';
export class StatsUi extends BaseUi {
private lastValues = {
totalSpace: '',
spaceReleased: '',
};
private timeouts = {
totalSpace: setTimeout(() => {}),
spaceReleased: setTimeout(() => {}),
};
private lastResultTypesValues: Map<ResultTypeRowKey, string> = new Map();
private resultTypesTimeouts: Map<ResultTypeRowKey, NodeJS.Timeout> =
new Map();
constructor(
private readonly config: IConfig,
private readonly resultsService: ResultsService,
private readonly logger: LoggerService,
) {
super();
}
// Prevent bug where the "Releasable space" and "Saved Space" got o 0.
reset(): void {
this.lastValues = {
totalSpace: '',
spaceReleased: '',
};
this.lastResultTypesValues.clear();
}
render(): void {
const { totalSpace, spaceReleased, resultsTypesCount } =
this.resultsService.getStats();
this.showStat({
description: INFO_MSGS.TOTAL_SPACE,
value: totalSpace,
lastValueKey: 'totalSpace',
position: UI_POSITIONS.TOTAL_SPACE,
updateColor: 'yellow',
});
this.showStat({
description: INFO_MSGS.SPACE_RELEASED,
value: spaceReleased,
lastValueKey: 'spaceReleased',
position: UI_POSITIONS.SPACE_RELEASED,
updateColor: 'green',
});
if (this.config.showErrors) {
this.showErrorsCount();
}
this.showResultsTypesCount(resultsTypesCount);
this.showActivePreset();
}
/** Print the value of the stat and if it is a different value from the
* previous run, highlight it for a while.
*/
private showStat({
description,
value,
lastValueKey,
position,
updateColor,
}: ShowStatProps): void {
const statPosition = { ...position };
statPosition.x += description.length;
if (value !== this.lastValues[lastValueKey]) {
// If is first render, initialize.
if (!this.lastValues[lastValueKey]) {
this.printAt(value, statPosition);
this.lastValues[lastValueKey] = value;
return;
}
this.printAt(pc[updateColor](`${value} ▲`), statPosition);
if (this.timeouts[lastValueKey]) {
clearTimeout(this.timeouts[lastValueKey]);
}
this.timeouts[lastValueKey] = setTimeout(() => {
this.printAt(value + ' ', statPosition);
}, 700);
this.lastValues[lastValueKey] = value;
} else {
this.printAt(value, statPosition);
}
}
private showErrorsCount(): void {
const errors = this.logger.get('error').length;
if (errors === 0) {
return;
}
const text = `${errors} error${errors > 1 ? 's' : ''}. 'e' to see`;
this.printAt(pc.yellow(text), { ...UI_POSITIONS.ERRORS_COUNT });
}
private showActivePreset(): void {
const MIN_TERMINAL_WIDTH = 94;
if (this.terminal.columns < MIN_TERMINAL_WIDTH) {
return;
}
const RIGHT_MARGIN = 2;
const CLEAR_LENGTH = 50;
const xStartClear = this.terminal.columns - CLEAR_LENGTH - RIGHT_MARGIN;
const clearText = ' '.repeat(CLEAR_LENGTH);
this.printAt(clearText, { x: xStartClear, y: 0 });
if (!this.config.profiles || this.config.profiles.length === 0) {
return;
}
const text = `[${this.config.profiles.join(', ')}]`;
const xPosition = this.terminal.columns - text.length - RIGHT_MARGIN;
this.printAt(pc.gray(pc.bold(text)), { x: xPosition, y: 0 });
}
private showResultsTypesCount(
resultsTypesCount: Array<{ type: string; count: number }>,
): void {
const MAX_CONTENT_LENGTH = 20;
const RIGHT_MARGIN = 2;
const MIN_TERMINAL_WIDTH = 94;
const START_Y = 1;
const NUM_ROWS = 5;
if (this.terminal.columns < MIN_TERMINAL_WIDTH) {
return;
}
const clearText = ' '.repeat(MAX_CONTENT_LENGTH);
const xStart = this.terminal.columns - MAX_CONTENT_LENGTH - RIGHT_MARGIN;
for (let i = 0; i < NUM_ROWS; i++) {
const yPos = START_Y + i;
this.printAt(clearText, { x: xStart, y: yPos });
}
const positions: { key: ResultTypeRowKey; yPosition: number }[] = [
{ key: 'row1', yPosition: 1 },
{ key: 'row2', yPosition: 2 },
{ key: 'row3', yPosition: 3 },
{ key: 'row4', yPosition: 4 },
{ key: 'row5', yPosition: 5 },
];
const maxRows = 5;
if (resultsTypesCount.length <= maxRows) {
resultsTypesCount.forEach((item, index) => {
const { key, yPosition } = positions[index];
const text = this.formatResultTypeText(
item.count,
item.type,
MAX_CONTENT_LENGTH,
);
const xPosition = this.terminal.columns - text.length - RIGHT_MARGIN;
this.showResultTypeRow(key, text, { x: xPosition, y: yPosition });
});
} else {
const topTypes = resultsTypesCount.slice(0, 4);
const remainingTypes = resultsTypesCount.slice(4);
topTypes.forEach((item, index) => {
const { key, yPosition } = positions[index];
const text = this.formatResultTypeText(
item.count,
item.type,
MAX_CONTENT_LENGTH,
);
const xPosition = this.terminal.columns - text.length - RIGHT_MARGIN;
this.showResultTypeRow(key, text, { x: xPosition, y: yPosition });
});
// Show summary in 5th row
const totalRemaining = remainingTypes.reduce(
(sum, item) => sum + item.count,
0,
);
const { key, yPosition } = positions[4];
const summaryText = `[+${remainingTypes.length}·total ${totalRemaining}]`;
const trimmedSummary =
summaryText.length > MAX_CONTENT_LENGTH
? summaryText.substring(0, MAX_CONTENT_LENGTH - 3) + '...'
: summaryText;
const xPosition =
this.terminal.columns - trimmedSummary.length - RIGHT_MARGIN;
this.showResultTypeRow(key, trimmedSummary, {
x: xPosition,
y: yPosition,
});
}
}
private formatResultTypeText(
count: number,
type: string,
maxLength: number,
): string {
const countStr = count.toString();
const baseLength = countStr.length + 3; // ' (' and ')'
const fullText = `${type} (${countStr})`;
if (fullText.length <= maxLength) {
return fullText;
}
const maxTypeLength = maxLength - baseLength;
const trimmedType =
type.length > maxTypeLength
? type.substring(0, maxTypeLength - 3) + '...'
: type;
return `${trimmedType} (${countStr})`;
}
private showResultTypeRow(
rowKey: ResultTypeRowKey,
text: string,
position: IPosition,
): void {
const lastValue = this.lastResultTypesValues.get(rowKey);
const valueChanged = text !== lastValue;
const hasActiveHighlight = this.resultTypesTimeouts.has(rowKey);
const shouldHighlight = valueChanged && lastValue !== undefined;
if (shouldHighlight) {
this.printAt(pc.white(text), { ...position });
const previousTimeout = this.resultTypesTimeouts.get(rowKey);
if (previousTimeout) {
clearTimeout(previousTimeout);
}
const timeout = setTimeout(() => {
this.printAt(pc.gray(text), { ...position });
this.resultTypesTimeouts.delete(rowKey);
}, 300);
this.resultTypesTimeouts.set(rowKey, timeout);
} else if (hasActiveHighlight) {
this.printAt(pc.white(text), { ...position });
} else {
this.printAt(pc.gray(text), { ...position });
}
this.lastResultTypesValues.set(rowKey, text);
}
}
================================================
FILE: src/cli/ui/components/header/status.ui.ts
================================================
import { BaseUi } from '../../base.ui.js';
import pc from 'picocolors';
import { SpinnerService } from '../../../services/spinner.service.js';
import { interval, Subject, takeUntil } from 'rxjs';
import { INFO_MSGS } from '../../../../constants/messages.constants.js';
import {
SPINNERS,
SPINNER_INTERVAL,
} from '../../../../constants/spinner.constants.js';
import { UI_POSITIONS } from '../../../../constants/main.constants.js';
import { ScanStatus } from '@core/interfaces/search-status.model.js';
import {
BAR_PARTS,
BAR_WIDTH,
} from '../../../../constants/status.constants.js';
export class StatusUi extends BaseUi {
private text = '';
private barNormalizedWidth = 0;
private barClosing = false;
private showProgressBar = true;
private pendingTasksPosition = { ...UI_POSITIONS.PENDING_TASKS };
private searchEnd$ = new Subject();
private readonly SEARCH_STATES = {
stopped: () => this.startingSearch(),
scanning: () => this.continueSearching(),
dead: () => this.fatalError(),
finished: () => this.continueFinishing(),
};
constructor(
private readonly spinnerService: SpinnerService,
private readonly searchStatus: ScanStatus,
) {
super();
}
start(): void {
this.barClosing = false;
this.showProgressBar = true;
this.spinnerService.setSpinner(SPINNERS.W10);
interval(SPINNER_INTERVAL)
.pipe(takeUntil(this.searchEnd$))
.subscribe(() => {
this.SEARCH_STATES[this.searchStatus.workerStatus]();
});
this.animateProgressBar();
}
reset(): void {
this.barClosing = false;
this.showProgressBar = true;
this.barNormalizedWidth = 0;
this.text = '';
this.pendingTasksPosition = { ...UI_POSITIONS.PENDING_TASKS };
this.searchEnd$.next(true);
this.searchEnd$.complete();
this.searchEnd$ = new Subject();
if (this.activeAnimation) {
clearTimeout(this.activeAnimation);
}
this.clearPendingTasks();
this.render();
}
completeSearch(duration: number): void {
this.searchEnd$.next(true);
this.searchEnd$.complete();
this.text = pc.green(INFO_MSGS.SEARCH_COMPLETED) + pc.gray(`${duration}s`);
this.render();
if (this.activeAnimation) {
clearTimeout(this.activeAnimation);
}
this.activeAnimation = setTimeout(() => this.animateClose(), 2000);
}
render(): void {
this.printAt(this.text + ' ', UI_POSITIONS.STATUS);
if (this.showProgressBar) {
this.renderProgressBar();
}
this.renderPendingTasks();
}
private renderPendingTasks(): void {
this.clearPendingTasks();
if (this.searchStatus.pendingDeletions === 0) {
return;
}
const { pendingDeletions } = this.searchStatus;
const text = pendingDeletions > 1 ? 'pending tasks' : 'pending task ';
this.printAt(
pc.yellow(`${pendingDeletions} ${text}`),
this.pendingTasksPosition,
);
}
private clearPendingTasks(): void {
const PENDING_TASK_LENGHT = 17;
this.printAt(' '.repeat(PENDING_TASK_LENGHT), this.pendingTasksPosition);
}
private renderProgressBar(): void {
const {
pendingSearchTasks,
completedSearchTasks,
completedStatsCalculation,
pendingStatsCalculation,
} = this.searchStatus;
const proportional = (a: number, b: number, c: number): number => {
if (c === 0) {
return 0;
}
return (a * b) / c;
};
const modifier =
this.barNormalizedWidth === 1
? 1
: // easeInOut formula
-(Math.cos(Math.PI * this.barNormalizedWidth) - 1) / 2;
const barSearchMax = pendingSearchTasks + completedSearchTasks;
const barStatsMax = pendingStatsCalculation + completedStatsCalculation;
let barLenght = Math.ceil(BAR_WIDTH * modifier);
let searchBarLenght = proportional(
completedSearchTasks,
BAR_WIDTH,
barSearchMax,
);
searchBarLenght = Math.ceil(searchBarLenght * modifier);
let doneBarLenght = proportional(
completedStatsCalculation,
searchBarLenght,
barStatsMax,
);
doneBarLenght = Math.floor(doneBarLenght * modifier);
barLenght -= searchBarLenght;
searchBarLenght -= doneBarLenght;
barLenght = Math.max(0, barLenght);
searchBarLenght = Math.max(0, searchBarLenght);
doneBarLenght = Math.max(0, doneBarLenght);
// Debug
// this.printAt(
// `V: ${barSearchMax},T: ${barLenght},C: ${searchBarLenght},D:${doneBarLenght} `,
// { x: 60, y: 5 },
// );
const progressBar =
BAR_PARTS.completed.repeat(doneBarLenght) +
BAR_PARTS.searchTask.repeat(searchBarLenght) +
BAR_PARTS.bg.repeat(barLenght);
this.printProgressBar(progressBar);
}
private activeAnimation: NodeJS.Timeout | null = null;
private animateProgressBar(): void {
if (this.barNormalizedWidth > 1) {
this.barNormalizedWidth = 1;
return;
}
this.barNormalizedWidth += 0.05;
this.renderProgressBar();
if (this.activeAnimation) {
clearTimeout(this.activeAnimation);
}
this.activeAnimation = setTimeout(
() => this.animateProgressBar(),
SPINNER_INTERVAL,
);
}
private animateClose(): void {
this.barClosing = true;
if (this.barNormalizedWidth < 0) {
this.barNormalizedWidth = 0;
this.showProgressBar = false;
this.movePendingTaskToTop();
return;
}
this.barNormalizedWidth -= 0.05;
this.renderProgressBar();
if (this.activeAnimation) {
clearTimeout(this.activeAnimation);
}
this.activeAnimation = setTimeout(
() => this.animateClose(),
SPINNER_INTERVAL,
);
}
/** When the progress bar disappears, "pending tasks" will move up one
position. */
private movePendingTaskToTop(): void {
this.clearPendingTasks();
this.pendingTasksPosition = { ...UI_POSITIONS.STATUS_BAR };
this.renderPendingTasks();
}
private printProgressBar(progressBar: string): void {
if (this.barClosing) {
const postX =
UI_POSITIONS.STATUS_BAR.x -
1 +
Math.round((BAR_WIDTH / 2) * (1 - this.barNormalizedWidth));
// Clear previus bar
this.printAt(' '.repeat(BAR_WIDTH), UI_POSITIONS.STATUS_BAR);
this.printAt(progressBar, {
x: postX,
y: UI_POSITIONS.STATUS_BAR.y,
});
} else {
this.printAt(progressBar, UI_POSITIONS.STATUS_BAR);
}
}
private startingSearch(): void {
this.text = INFO_MSGS.STARTING;
this.render();
}
private continueSearching(): void {
this.text = INFO_MSGS.SEARCHING + this.spinnerService.nextFrame();
this.render();
}
private fatalError(): void {
this.text = pc.red(INFO_MSGS.FATAL_ERROR);
this.searchEnd$.next(true);
this.searchEnd$.complete();
this.render();
}
private continueFinishing(): void {
this.text = INFO_MSGS.CALCULATING_STATS + this.spinnerService.nextFrame();
this.render();
}
}
================================================
FILE: src/cli/ui/components/help/help-command.ui.ts
================================================
import {
HELP_HEADER,
OPTIONS,
HELP_FOOTER,
HELP_PROGRESSBAR,
} from '../../../../constants/cli.constants.js';
import { MARGINS, UI_HELP } from '../../../../constants/main.constants.js';
import { INFO_MSGS } from '../../../../constants/messages.constants.js';
import { ConsoleService } from '../../../services/console.service.js';
import { BaseUi } from '../../base.ui.js';
import pc from 'picocolors';
export class HelpCommandUi extends BaseUi {
constructor(private readonly consoleService: ConsoleService) {
super();
}
render(): void {
throw new Error('Method not implemented.');
}
show(): void {
const maxWidth = Math.min(UI_HELP.MAX_WIDTH, this.terminal.columns);
this.clear();
this.print(pc.inverse(pc.bold(INFO_MSGS.HELP_TITLE + '\n')));
const headerLines = this.consoleService.splitWordsByWidth(
HELP_HEADER,
maxWidth,
);
headerLines.forEach((line) => this.print(line + '\n'));
this.print('\n');
const progressBarLines = this.consoleService.splitWordsByWidth(
HELP_PROGRESSBAR,
maxWidth,
);
progressBarLines.forEach((line) => this.print(line + '\n'));
this.print('\n');
const maxDescriptionWidth = Math.min(
maxWidth - UI_HELP.X_DESCRIPTION_OFFSET,
this.terminal.columns - UI_HELP.X_DESCRIPTION_OFFSET,
);
this.print(pc.black(pc.bgYellow(pc.bold(' Options '))) + '\n');
OPTIONS.forEach((option) => {
const args = option.arg.reduce((text, arg) => text + ', ' + arg);
const padding = ' '.repeat(UI_HELP.X_COMMAND_OFFSET);
const commandLength = UI_HELP.X_COMMAND_OFFSET + args.length;
const commandTooLong = commandLength >= UI_HELP.X_DESCRIPTION_OFFSET;
if (commandTooLong) {
this.print(padding + args + '\n');
} else {
this.print(padding + args);
}
const description = this.consoleService.splitWordsByWidth(
option.description,
maxDescriptionWidth,
);
description.forEach((line, index) => {
if (index === 0 && !commandTooLong) {
const spaceBetween = ' '.repeat(
UI_HELP.X_DESCRIPTION_OFFSET - commandLength,
);
this.print(spaceBetween + line + '\n');
} else {
const descriptionPadding = ' '.repeat(UI_HELP.X_DESCRIPTION_OFFSET);
this.print(descriptionPadding + line + '\n');
}
});
this.print('\n');
});
this.print('\n');
const footerLines = this.consoleService.splitWordsByWidth(
HELP_FOOTER,
maxWidth,
);
footerLines.forEach((line) => this.print(line + '\n'));
}
clear(): void {
for (let row = MARGINS.ROW_RESULTS_START; row < this.terminal.rows; row++) {
this.clearLine(row);
}
}
}
================================================
FILE: src/cli/ui/components/help/help.constants.ts
================================================
/* eslint-disable quotes */
import pc from 'picocolors';
interface HelpSection {
id: string;
title: string;
icon: string;
content: string[];
}
export const HELP_SECTIONS: HelpSection[] = [
{
id: 'welcome',
title: 'Welcome',
icon: '👋',
content: [
pc.bold('Welcome to npkill!'),
'',
'Npkill helps you find and manage "junk" directories',
'left behind by development tools.',
'',
'These folders are essential while working on projects,',
'but over time they pile up, eating tons of space long',
"after you've moved on.",
'',
'Npkill scans your directories, lists these folders with',
'their sizes, and shows when you last touched each project,',
'so you can quickly decide what to keep and what to clean.',
'',
pc.green(pc.bold('Easy and powerful!')),
],
},
{
id: 'shortcuts',
title: 'Quick Reference',
icon: '⌨️',
content: [
pc.cyan(pc.bold('Navigation')),
` ${pc.green('↑/↓ or j/k')} Move cursor.`,
` ${pc.green('←/→ or h/l')} Switch panels.`,
` ${pc.green('PgUp/PgDown')} Fast scroll.`,
` ${pc.green('Home/End')} Jump to first/last.`,
'',
pc.cyan(pc.bold('Actions (normal mode)')),
` ${pc.green('SPACE / DEL')} Delete folder.`,
` ${pc.green('o')} Open parent folder.`,
` ${pc.green('/')} Search (Regex supported).`,
` ${pc.green('t')} Enter ${pc.green('multi-select mode')}.`,
'',
pc.cyan(pc.bold('Actions (multi-select mode)')),
` ${pc.green('t')} Back to ${pc.green('normal mode')} without delete.`,
` ${pc.green('v')} Range selection.`,
` ${pc.green('a')} Select/deselect all.`,
` ${pc.green('SPACE')} Select/deselect current.`,
` ${pc.green('ENTER')} Delete selected folders.`,
'',
pc.cyan(pc.bold('Others')),
` ${pc.green('e')} Show errors.`,
` ${pc.green('q / ESC')} Quit npkill.`,
],
},
{
id: 'header',
title: 'Header Info',
icon: '📊',
content: [
pc.bold('Understanding the header'),
'',
pc.bold(pc.green('Potential Space')),
'The total size of all detected directories.',
'This represents the maximum possible space you could',
'free if you deleted everything.',
'',
pc.bold(pc.green('Freed Space')),
'The space actually recovered in this session.',
'',
pc.bold(pc.green('Last Modified (age)')),
'Shows when the last file in the parent workspace was',
'modified. This helps identify abandoned projects.',
'',
pc.dim('Note: This checks the entire parent directory,'),
pc.dim('not just the target folder itself.'),
'',
pc.bold(pc.green('Progress Bar')),
'The progress bar has 3 color-coded parts:',
'',
` ${pc.green('▀▀▀▀')} Green → Results ready (stats calculated)`,
` ${pc.white('▀▀▀▀')} White → Directories examined`,
` ${pc.gray('▀▀▀▀')} Gray → Pending to be analyzed`,
'',
'Full bar example:',
` ${pc.green('▀▀▀▀▀▀▀')}${pc.white('▀▀▀▀')}${pc.gray('▀▀▀▀▀▀▀▀▀▀▀')}`,
],
},
{
id: 'warnings',
title: 'Warnings',
icon: '⚠️',
content: [
pc.bold(pc.yellow('Important: Not all results are safe to delete!')),
'',
'Some applications (VSCode, Discord, Slack, etc.) need',
'their dependencies to work. If their directory is deleted,',
'the application will probably break until dependencies',
'are reinstalled.',
'',
'Npkill will try to detect this folders and show "⚠️"',
'alongside the result and mark it as "sensitive".',
'',
'',
pc.bold(pc.green('Pro tip')),
'Use the Info panel (→) to see more information',
'about why a folder is flagged.',
],
},
{
id: 'panels',
title: 'Panels',
icon: '📋',
content: [
pc.bold('Understanding the interface'),
'',
pc.bold(pc.cyan('Delete Panel (Main)')),
'Lists all found directories with their sizes.',
'Navigate with arrow keys or j/k.',
'Press SPACE to delete a folder.',
'Press → to see details.',
'',
pc.bold(pc.cyan('Info Panel')),
'Shows details of the selected result. Plus notes',
'that might be useful to you.',
'',
pc.bold(pc.cyan('Options Panel')),
'Configure npkill settings on the fly.',
'',
pc.bold(pc.cyan('Help Panel (You are here!)')),
'Navigate through help sections.',
'Use ↑/↓ to change sections.',
'Scroll content with j/k or arrow keys.',
],
},
// { // TODO pending to add .npkillrc support
// id: 'config',
// title: 'Configuration',
// icon: '⚙️',
// content: [
// colors.bold('Customizing npkill with .npkillrc'),
// '',
// colors.bold.cyan('What is .npkillrc?'),
// 'A configuration file where you can set your default',
// 'preferences, custom profiles, and target folders.',
// '',
// colors.bold.cyan('Location'),
// '',
// 'Place it in your home directory:',
// colors.dim(' ~/.npkillrc'),
// '',
// 'Or load from a custom location:',
// colors.green(' npkill --config /path/to/config'),
// '',
// colors.bold.cyan('Example Configuration'),
// '',
// colors.dim('{'),
// colors.dim(' "targets": ["node_modules", ".venv", "target"],'),
// colors.dim(' "exclude": [".git", "important-project"],'),
// colors.dim(' "sortBy": "size",'),
// colors.dim(' "excludeSensitiveResults": true,'),
// colors.dim(' "profiles": {'),
// colors.dim(' "mystack": {'),
// colors.dim(' "targets": ["node_modules", "venv", "target"]'),
// colors.dim(' }'),
// colors.dim(' }'),
// colors.dim('}'),
// '',
// colors.bold.cyan('Available Options'),
// `${colors.green('targets')} Target folder names to search`,
// `${colors.green('exclude')} Directories to skip`,
// `${colors.green('sortBy')} Default sort (size/path/age)`,
// `${colors.green('sizeUnit')} Display unit (auto/mb/gb)`,
// `${colors.green('profiles')} Custom profile definitions`,
// ],
// },
{
id: 'profiles',
title: 'Profiles',
icon: '📦',
content: [
pc.bold('Working with profiles'),
'',
pc.bold(pc.cyan('What are profiles?')),
'Profiles are presets of target folders for different',
'programming languages and tools.',
'',
pc.bold(pc.cyan('Usage')
gitextract_fg_l4s4m/ ├── .github/ │ ├── CODE_OF_CONDUCT.es.md │ ├── CODE_OF_CONDUCT.md │ ├── CONTRIBUTING.es.md │ ├── CONTRIBUTING.md │ ├── FUNDING.yml │ ├── ISSUE_TEMPLATE/ │ │ ├── bug-report.md │ │ └── feature_request.md │ └── workflows/ │ ├── codeql-analysis.yml │ └── nodejs.yml ├── .gitignore ├── .husky/ │ ├── commit-msg │ └── pre-commit ├── .node-version ├── .npmignore ├── .prettierrc ├── .vscode/ │ ├── launch.json │ └── settings.json ├── API.md ├── LICENSE ├── README.es.md ├── README.id.md ├── README.md ├── README.pt.md ├── README.tr.md ├── docs/ │ ├── RELEASE.md │ ├── create-demo.sh │ ├── json-output.md │ ├── npkillrc.md │ └── profiles.md ├── eslint.config.mjs ├── jest.config.ts ├── package.json ├── src/ │ ├── cli/ │ │ ├── cli.controller.ts │ │ ├── interfaces/ │ │ │ ├── cli-options.interface.ts │ │ │ ├── command-keys.interface.ts │ │ │ ├── config.interface.ts │ │ │ ├── index.ts │ │ │ ├── json-output.interface.ts │ │ │ ├── key-press.interface.ts │ │ │ ├── node-version.interface.ts │ │ │ ├── stats.interface.ts │ │ │ ├── ui-positions.interface.ts │ │ │ └── version.interface.ts │ │ ├── models/ │ │ │ └── start-parameters.model.ts │ │ ├── services/ │ │ │ ├── console.service.ts │ │ │ ├── https.service.ts │ │ │ ├── index.ts │ │ │ ├── json-output.service.ts │ │ │ ├── results.service.ts │ │ │ ├── scan.service.ts │ │ │ ├── spinner.service.ts │ │ │ ├── ui.service.ts │ │ │ └── update.service.ts │ │ └── ui/ │ │ ├── base.ui.ts │ │ ├── components/ │ │ │ ├── general.ui.ts │ │ │ ├── header/ │ │ │ │ ├── header-ui.constants.ts │ │ │ │ ├── header.ui.ts │ │ │ │ ├── stats.ui.ts │ │ │ │ └── status.ui.ts │ │ │ ├── help/ │ │ │ │ ├── help-command.ui.ts │ │ │ │ ├── help.constants.ts │ │ │ │ └── help.ui.ts │ │ │ ├── logs.ui.ts │ │ │ ├── options.ui.ts │ │ │ ├── result-details.ui.ts │ │ │ ├── results.ui.ts │ │ │ └── warning.ui.ts │ │ ├── heavy.ui.ts │ │ └── index.ts │ ├── constants/ │ │ ├── cli.constants.ts │ │ ├── index.ts │ │ ├── main.constants.ts │ │ ├── messages.constants.ts │ │ ├── options.constants.ts │ │ ├── os-service-map.constants.ts │ │ ├── result-descriptions.constants.ts │ │ ├── sort.result.ts │ │ ├── spinner.constants.ts │ │ ├── status.constants.ts │ │ ├── update.constants.ts │ │ └── workers.constants.ts │ ├── core/ │ │ ├── constants/ │ │ │ ├── global-ignored.constants.ts │ │ │ ├── index.ts │ │ │ └── profiles.constants.ts │ │ ├── index.ts │ │ ├── interfaces/ │ │ │ ├── file-service.interface.ts │ │ │ ├── folder.interface.ts │ │ │ ├── index.ts │ │ │ ├── logger-service.interface.ts │ │ │ ├── npkill.interface.ts │ │ │ ├── npkillrc-config.interface.ts │ │ │ ├── profile.interface.ts │ │ │ ├── search-status.model.ts │ │ │ └── services.interface.ts │ │ ├── npkill.ts │ │ └── services/ │ │ ├── config/ │ │ │ ├── config-merger.ts │ │ │ ├── config-validator.ts │ │ │ ├── index.ts │ │ │ ├── profile-validator.ts │ │ │ └── property-validators.ts │ │ ├── config.service.ts │ │ ├── files/ │ │ │ ├── files.service.ts │ │ │ ├── files.worker.service.ts │ │ │ ├── files.worker.ts │ │ │ ├── index.ts │ │ │ ├── unix-files.service.ts │ │ │ └── windows-files.service.ts │ │ ├── index.ts │ │ ├── logger.service.ts │ │ ├── profiles.service.ts │ │ └── stream.service.ts │ ├── dirname.ts │ ├── index.ts │ ├── main.ts │ └── utils/ │ ├── get-file-content.ts │ ├── is-safe-to-delete.ts │ └── unit-conversions.ts ├── stryker.conf.js ├── tests/ │ ├── cli/ │ │ ├── cli.controller.test.ts │ │ ├── services/ │ │ │ ├── console.service.test.ts │ │ │ ├── https.service.test.ts │ │ │ ├── json-output.service.test.ts │ │ │ ├── profiles.service.test.ts │ │ │ ├── result.service.test.ts │ │ │ ├── scan.service.test.ts │ │ │ ├── spinner.service.test.ts │ │ │ ├── ui.service.test.ts │ │ │ └── update.service.test.ts │ │ └── ui/ │ │ └── results.ui.test.ts │ ├── core/ │ │ ├── npkill.test.ts │ │ └── services/ │ │ ├── config.service.test.ts │ │ ├── files/ │ │ │ ├── files.service.test.ts │ │ │ ├── files.worker.service.test.ts │ │ │ └── files.worker.test.ts │ │ └── logger.service.test.ts │ ├── index.test.ts │ ├── main.test.ts │ └── utils/ │ └── utils.test.ts ├── tsconfig.json └── tslint.json
SYMBOL INDEX (511 symbols across 78 files)
FILE: src/cli/cli.controller.ts
class CliController (line 50) | class CliController {
method constructor (line 65) | constructor(
method init (line 81) | init(): void {
method showDeleteAllWarning (line 132) | private showDeleteAllWarning(): void {
method initUi (line 137) | private initUi(): void {
method openOptions (line 175) | private openOptions(): void {
method openResultsDetails (line 240) | private openResultsDetails(folder: CliScanFoundFolder): void {
method loadConfigFile (line 265) | private loadConfigFile(): void {
method parseArguments (line 347) | private parseArguments(): void {
method showErrorPopup (line 546) | private showErrorPopup(visible: boolean): void {
method invalidSortParam (line 562) | private invalidSortParam(): void {
method showHelp (line 568) | private showHelp(): void {
method showProgramVersion (line 572) | private showProgramVersion(): void {
method isValidColor (line 576) | private isValidColor(color: string): boolean {
method isValidSortParam (line 580) | private isValidSortParam(sortName: string): boolean {
method isValidSizeUnit (line 584) | private isValidSizeUnit(sizeUnit: string): boolean {
method invalidSizeUnitParam (line 588) | private invalidSizeUnitParam(): void {
method getVersion (line 594) | private getVersion(): string {
method prepareScreen (line 601) | private prepareScreen(): void {
method checkRequirements (line 609) | private checkRequirements(): void {
method checkScreenRequirements (line 614) | private checkScreenRequirements(): void {
method checkFileRequirements (line 622) | private checkFileRequirements(): void {
method checkVersion (line 633) | private checkVersion(): void {
method showUpdateMessage (line 653) | private showUpdateMessage(): void {
method isTerminalTooSmall (line 658) | private isTerminalTooSmall(): boolean {
method printFoldersSection (line 662) | private printFoldersSection(): void {
method setupEventsListener (line 666) | private setupEventsListener(): void {
method keyPress (line 689) | private keyPress(key: IKeyPress): void {
method scan (line 704) | private scan(): void {
method initializeScan (line 716) | private initializeScan(): void {
method scanInJson (line 722) | private scanInJson(): void {
method scanInUiMode (line 749) | private scanInUiMode(): void {
method setupJsonModeSignalHandlers (line 775) | private setupJsonModeSignalHandlers(): void {
method processNodeFolderForUi (line 785) | private processNodeFolderForUi(nodeFolder: CliScanFoundFolder): void {
method processFolderStatsForUi (line 797) | private processFolderStatsForUi(folder: CliScanFoundFolder): void {
method finishFolderStats (line 806) | private finishFolderStats(): void {
method completeSearch (line 817) | private completeSearch(): void {
method setSearchDuration (line 823) | private setSearchDuration(): void {
method isQuitKey (line 827) | private isQuitKey(ctrl: boolean, name: string): boolean {
method exitWithError (line 831) | private exitWithError(): void {
method exitGracefully (line 839) | private exitGracefully(): void {
method quit (line 847) | private quit(): void {
method resetConsoleState (line 859) | private resetConsoleState(): void {
method printExitMessage (line 865) | private printExitMessage(): void {
method deleteFolder (line 870) | private deleteFolder(folder: CliScanFoundFolder): void {
method newError (line 905) | private newError(error: string): void {
FILE: src/cli/interfaces/cli-options.interface.ts
type ICliOptions (line 1) | interface ICliOptions {
FILE: src/cli/interfaces/command-keys.interface.ts
type IKeysCommand (line 1) | interface IKeysCommand {
FILE: src/cli/interfaces/config.interface.ts
type IConfig (line 1) | interface IConfig {
FILE: src/cli/interfaces/json-output.interface.ts
type JsonOutputBase (line 3) | interface JsonOutputBase {
type JsonCliScanFoundFolder (line 7) | interface JsonCliScanFoundFolder
type JsonStreamOutput (line 14) | interface JsonStreamOutput extends JsonOutputBase {
type JsonSimpleOutput (line 22) | interface JsonSimpleOutput extends JsonOutputBase {
type JsonErrorOutput (line 31) | interface JsonErrorOutput extends JsonOutputBase {
FILE: src/cli/interfaces/key-press.interface.ts
type IKeyPress (line 1) | interface IKeyPress {
FILE: src/cli/interfaces/node-version.interface.ts
type INodeVersion (line 1) | interface INodeVersion {
FILE: src/cli/interfaces/stats.interface.ts
type CliScanFoundFolder (line 3) | interface CliScanFoundFolder extends ScanFoundFolder {
type IResultTypeCount (line 9) | interface IResultTypeCount {
type IStats (line 14) | interface IStats {
FILE: src/cli/interfaces/ui-positions.interface.ts
type IPosition (line 1) | interface IPosition {
type IUiPosition (line 6) | type IUiPosition = Record<string, IPosition>;
FILE: src/cli/interfaces/version.interface.ts
type IVersion (line 1) | interface IVersion {
FILE: src/cli/models/start-parameters.model.ts
class StartParameters (line 1) | class StartParameters {
method add (line 4) | add(key: string, value: string | boolean): void {
method isTrue (line 8) | isTrue(key: string): boolean {
method getString (line 13) | getString(key: string): string {
method getStrings (line 22) | getStrings(key: string): string[] {
FILE: src/cli/services/console.service.ts
class ConsoleService (line 8) | class ConsoleService {
method getParameters (line 9) | getParameters(rawArgv: string[]): StartParameters {
method splitWordsByWidth (line 36) | splitWordsByWidth(text: string, width: number): string[] {
method splitData (line 45) | splitData(data: string, separator = '\n'): string[] {
method replaceString (line 52) | replaceString(
method shortenText (line 60) | shortenText(text: string, width: number, startCut = 0): string {
method isRunningBuild (line 72) | isRunningBuild(): boolean {
method startListenKeyEvents (line 76) | startListenKeyEvents(): void {
method normalizeParams (line 85) | private normalizeParams(argvs: string[]): string[] {
method isValidShortenParams (line 89) | private isValidShortenParams(
method removeSystemArgvs (line 102) | private removeSystemArgvs(allArgv: string[]): string[] {
method isArgOption (line 106) | private isArgOption(argv: string): boolean {
method isArgHavingParams (line 110) | private isArgHavingParams(nextArgv: string): boolean {
method isValidOption (line 116) | private isValidOption(arg: string): boolean {
method getOption (line 120) | private getOption(arg: string): ICliOptions | undefined {
method isNegative (line 124) | private isNegative(numb: number): boolean {
FILE: src/cli/services/https.service.ts
class HttpsService (line 3) | class HttpsService {
method getJson (line 4) | async getJson(url: string): Promise<Record<string, string>> {
method isCorrectResponse (line 31) | private isCorrectResponse(statusCode: number): boolean {
FILE: src/cli/services/json-output.service.ts
class JsonOutputService (line 10) | class JsonOutputService {
method constructor (line 16) | constructor(
method initializeSession (line 21) | initializeSession(streamMode: boolean = false): void {
method processResult (line 27) | processResult(folder: CliScanFoundFolder): void {
method completeScan (line 35) | completeScan(): void {
method writeStreamResult (line 41) | private writeStreamResult(folder: CliScanFoundFolder): void {
method addResult (line 58) | private addResult(folder: CliScanFoundFolder): void {
method writeSimpleResults (line 62) | private writeSimpleResults(): void {
method writeError (line 84) | writeError(error: Error | string): void {
method getResultsCount (line 96) | getResultsCount(): number {
method handleShutdown (line 100) | handleShutdown(): void {
method sanitizeFolderForOutput (line 106) | private sanitizeFolderForOutput(
FILE: src/cli/services/results.service.ts
class ResultsService (line 10) | class ResultsService {
method addResult (line 14) | addResult(result: CliScanFoundFolder): void {
method sortResults (line 18) | sortResults(method: string): void {
method reset (line 22) | reset(): void {
method setSizeUnit (line 26) | setSizeUnit(sizeUnit: 'auto' | 'mb' | 'gb'): void {
method getStats (line 30) | getStats(): IStats {
FILE: src/cli/services/scan.service.ts
type CalculateFolderStatsOptions (line 24) | interface CalculateFolderStatsOptions {
class ScanService (line 28) | class ScanService {
method constructor (line 29) | constructor(private readonly npkill: Npkill) {}
method scan (line 31) | scan(config: IConfig): Observable<CliScanFoundFolder> {
method calculateFolderStats (line 63) | calculateFolderStats(
method isExcludedDangerousDirectory (line 130) | private isExcludedDangerousDirectory(
FILE: src/cli/services/spinner.service.ts
class SpinnerService (line 1) | class SpinnerService {
method setSpinner (line 5) | setSpinner(spinner: string[]): void {
method nextFrame (line 10) | nextFrame(): string {
method reset (line 15) | reset(): void {
method updateCount (line 19) | private updateCount(): void {
method isLastFrame (line 27) | private isLastFrame(): boolean {
FILE: src/cli/services/ui.service.ts
class UiService (line 4) | class UiService {
method setRawMode (line 9) | setRawMode(set = true): void {
method setCursorVisible (line 16) | setCursorVisible(visible: boolean): void {
method add (line 23) | add(component: BaseUi): void {
method remove (line 27) | remove(baseUiId: string): void {
method renderAll (line 31) | renderAll(): void {
method setFreezeAll (line 40) | setFreezeAll(freeze: boolean): void {
method setVisibleAll (line 46) | setVisibleAll(visible: boolean): void {
method clear (line 52) | clear(): void {
method print (line 56) | print(text: string): void {
method printAt (line 60) | printAt(message: string, position: Position): void {
method setCursorAt (line 65) | setCursorAt({ x, y }: Position): void {
method clearLine (line 69) | clearLine(row: number): void {
FILE: src/cli/services/update.service.ts
class UpdateService (line 8) | class UpdateService {
method constructor (line 9) | constructor(private readonly httpsService: HttpsService) {}
method isUpdated (line 15) | async isUpdated(localVersion: string): Promise<boolean> {
method compareVersions (line 24) | private compareVersions(local: string, remote: string): boolean {
method getRemoteVersion (line 31) | private async getRemoteVersion(): Promise<string> {
method isSameVersion (line 36) | private isSameVersion(version1: string, version2: string): boolean {
method isLocalVersionGreater (line 41) | private isLocalVersionGreater(local: string, remote: string): boolean {
FILE: src/cli/ui/base.ui.ts
type Position (line 4) | interface Position {
type InteractiveUi (line 9) | interface InteractiveUi {
method printAt (line 20) | protected printAt(message: string, position: Position): void {
method setCursorAt (line 25) | protected setCursorAt({ x, y }: Position): void {
method print (line 29) | protected print(text: string): void {
method clearLine (line 36) | protected clearLine(row: number): void {
method setPosition (line 40) | setPosition(position: Position, renderOnSet = true): void {
method setVisible (line 48) | setVisible(visible: boolean, renderOnSet = true): void {
method position (line 56) | get position(): Position {
method visible (line 60) | get visible(): boolean {
method terminal (line 64) | get terminal(): { columns: number; rows: number } {
FILE: src/cli/ui/components/general.ui.ts
class GeneralUi (line 6) | class GeneralUi extends BaseUi {
method render (line 7) | render(): void {}
method printExitMessage (line 9) | printExitMessage(stats: { spaceReleased: string }): void {
FILE: src/cli/ui/components/header/header-ui.constants.ts
type MENU_BAR_OPTIONS (line 1) | enum MENU_BAR_OPTIONS {
FILE: src/cli/ui/components/header/header.ui.ts
class HeaderUi (line 13) | class HeaderUi extends BaseUi {
method constructor (line 24) | constructor(private readonly config: IConfig) {
method setSearch (line 32) | setSearch(text: string | null, isInputActive = false) {
method render (line 45) | render(): void {
method renderHeader (line 78) | private renderHeader(): void {
method renderMenuBar (line 87) | private renderMenuBar(): void {
FILE: src/cli/ui/components/header/stats.ui.ts
type ShowStatProps (line 9) | interface ShowStatProps {
type ResultTypeRowKey (line 17) | type ResultTypeRowKey = 'row1' | 'row2' | 'row3' | 'row4' | 'row5';
class StatsUi (line 19) | class StatsUi extends BaseUi {
method constructor (line 34) | constructor(
method reset (line 43) | reset(): void {
method render (line 51) | render(): void {
method showStat (line 82) | private showStat({
method showErrorsCount (line 116) | private showErrorsCount(): void {
method showActivePreset (line 127) | private showActivePreset(): void {
method showResultsTypesCount (line 148) | private showResultsTypesCount(
method formatResultTypeText (line 225) | private formatResultTypeText(
method showResultTypeRow (line 247) | private showResultTypeRow(
FILE: src/cli/ui/components/header/status.ui.ts
class StatusUi (line 17) | class StatusUi extends BaseUi {
method constructor (line 31) | constructor(
method start (line 38) | start(): void {
method reset (line 51) | reset(): void {
method completeSearch (line 68) | completeSearch(duration: number): void {
method render (line 80) | render(): void {
method renderPendingTasks (line 90) | private renderPendingTasks(): void {
method clearPendingTasks (line 104) | private clearPendingTasks(): void {
method renderProgressBar (line 109) | private renderProgressBar(): void {
method animateProgressBar (line 172) | private animateProgressBar(): void {
method animateClose (line 189) | private animateClose(): void {
method movePendingTaskToTop (line 212) | private movePendingTaskToTop(): void {
method printProgressBar (line 218) | private printProgressBar(progressBar: string): void {
method startingSearch (line 236) | private startingSearch(): void {
method continueSearching (line 241) | private continueSearching(): void {
method fatalError (line 246) | private fatalError(): void {
method continueFinishing (line 253) | private continueFinishing(): void {
FILE: src/cli/ui/components/help/help-command.ui.ts
class HelpCommandUi (line 13) | class HelpCommandUi extends BaseUi {
method constructor (line 14) | constructor(private readonly consoleService: ConsoleService) {
method render (line 18) | render(): void {
method show (line 22) | show(): void {
method clear (line 89) | clear(): void {
FILE: src/cli/ui/components/help/help.constants.ts
type HelpSection (line 4) | interface HelpSection {
constant HELP_SECTIONS (line 11) | const HELP_SECTIONS: HelpSection[] = [
FILE: src/cli/ui/components/help/help.ui.ts
class HelpUi (line 8) | class HelpUi extends BaseUi implements InteractiveUi {
method constructor (line 36) | constructor() {
method previousSection (line 40) | private previousSection(): void {
method nextSection (line 48) | private nextSection(): void {
method selectSection (line 56) | private selectSection(): void {
method scrollUp (line 61) | private scrollUp(): void {
method scrollDown (line 68) | private scrollDown(): void {
method scrollPageUp (line 85) | private scrollPageUp(): void {
method scrollPageDown (line 91) | private scrollPageDown(): void {
method scrollToTop (line 104) | private scrollToTop(): void {
method scrollToBottom (line 109) | private scrollToBottom(): void {
method goToOptions (line 119) | private goToOptions(): void {
method getContentAreaHeight (line 124) | private getContentAreaHeight(): number {
method onKeyInput (line 128) | onKeyInput({ name }: IKeyPress): void {
method render (line 136) | render(): void {
method drawIndex (line 155) | private drawIndex(startRow: number): void {
method drawContent (line 192) | private drawContent(startRow: number, contentHeight: number): void {
method getStringWidth (line 229) | private getStringWidth(str: string): number {
method clear (line 234) | clear(): void {
FILE: src/cli/ui/components/logs.ui.ts
class LogsUi (line 8) | class LogsUi extends BaseUi implements InteractiveUi {
method constructor (line 20) | constructor(private readonly logger: LoggerService) {
method onKeyInput (line 25) | onKeyInput({ name }: IKeyPress): void {
method render (line 33) | render(): void {
method cyclePages (line 37) | private cyclePages(): void {
method close (line 48) | private close(): void {
method renderPopup (line 52) | private renderPopup(): void {
method printHeader (line 114) | private printHeader(): void {
method stylizeText (line 131) | private stylizeText(
method chunkString (line 140) | private chunkString(str: string, length: number): string[] {
method chunkArray (line 145) | private chunkArray(arr: string[], size: number): string[][] {
method calculatePosition (line 151) | private calculatePosition(): void {
FILE: src/cli/ui/components/options.ui.ts
type OptionType (line 11) | type OptionType = 'checkbox' | 'dropdown' | 'input';
type OptionItem (line 13) | interface OptionItem<K extends keyof IConfig = keyof IConfig> {
class OptionsUi (line 21) | class OptionsUi extends BaseUi implements InteractiveUi {
method constructor (line 48) | constructor(
method initializeOptions (line 57) | private initializeOptions(): void {
method move (line 110) | private move(dir: -1 | 1): void {
method activateSelected (line 117) | private activateSelected(): void {
method handleEditKey (line 163) | private handleEditKey(name: string, sequence: string): void {
method emitConfigChange (line 218) | private emitConfigChange<K extends keyof IConfig>(
method cancelEdit (line 226) | private cancelEdit(): void {
method onKeyInput (line 232) | onKeyInput(key: IKeyPress): void {
method goBack (line 242) | private goBack(): void {
method goToHelp (line 247) | private goToHelp(): void {
method render (line 252) | render(): void {
method printHintMessage (line 360) | private printHintMessage() {
method clear (line 378) | clear(): void {
FILE: src/cli/ui/components/result-details.ui.ts
class ResultDetailsUi (line 12) | class ResultDetailsUi extends BaseUi implements InteractiveUi {
method constructor (line 26) | constructor(
method openFolder (line 33) | private openFolder(): void {
method goBack (line 39) | private goBack(): void {
method onKeyInput (line 44) | onKeyInput({ name }: IKeyPress): void {
method render (line 52) | render(): void {
method clear (line 216) | clear(): void {
method getRowsAvailable (line 223) | private getRowsAvailable(): number {
method getRow (line 228) | private getRow(index: number): number {
FILE: src/cli/ui/components/results.ui.ts
constant CURSOR_ROW_COLOR (line 22) | const CURSOR_ROW_COLOR = 'bgBlue';
class ResultsUi (line 24) | class ResultsUi extends HeavyUi implements InteractiveUi {
method constructor (line 78) | constructor(
method openFolder (line 85) | private openFolder(): void {
method showDetails (line 91) | private showDetails(): void {
method goOptions (line 99) | private goOptions(): void {
method endNpkill (line 106) | private endNpkill(): void {
method toggleSelectMode (line 110) | private toggleSelectMode(): void {
method startRangeSelection (line 119) | private startRangeSelection(): void {
method toggleSelectAll (line 144) | private toggleSelectAll(): void {
method handleSpacePress (line 164) | private handleSpacePress(): void {
method toggleFolderSelection (line 173) | private toggleFolderSelection(): void {
method applyRangeSelection (line 186) | private applyRangeSelection(): void {
method deleteSelected (line 219) | private deleteSelected(): void {
method activateSearchInputMode (line 231) | private activateSearchInputMode(): void {
method handleSearchInput (line 237) | private handleSearchInput(key: IKeyPress): void {
method filterResults (line 276) | private filterResults(): void {
method onKeyInput (line 287) | onKeyInput(key: IKeyPress): void {
method render (line 309) | render(): void {
method clear (line 369) | clear(): void {
method completeSearch (line 376) | completeSearch(): void {
method printResults (line 383) | private printResults(): void {
method noResults (line 392) | private noResults(): void {
method printFolderRow (line 403) | private printFolderRow(folder: CliScanFoundFolder, row: number): void {
method rangeSelectedCursor (line 459) | private rangeSelectedCursor(row: number): void {
method selectionCursor (line 466) | private selectionCursor(row: number): void {
method getFolderTexts (line 474) | private getFolderTexts(folder: CliScanFoundFolder): {
method cursorUp (line 524) | cursorUp(): void {
method cursorDown (line 528) | cursorDown(): void {
method cursorPageUp (line 532) | cursorPageUp(): void {
method cursorPageDown (line 537) | cursorPageDown(): void {
method cursorFirstResult (line 542) | cursorFirstResult(): void {
method cursorLastResult (line 546) | cursorLastResult(): void {
method fitScroll (line 550) | fitScroll(): void {
method scrollFolderResults (line 582) | scrollFolderResults(scrollAmount: number): void {
method moveCursor (line 588) | private moveCursor(index: number): void {
method getFolderPathText (line 609) | private getFolderPathText(folder: CliScanFoundFolder): string {
method paintStatusFolderPath (line 650) | private paintStatusFolderPath(folderString: string, action: string): s...
method printScrollBar (line 674) | private printScrollBar(): void {
method isCursorInLowerLimit (line 705) | private isCursorInLowerLimit(): boolean {
method isCursorInUpperLimit (line 709) | private isCursorInUpperLimit(): boolean {
method getRealCursorPosY (line 713) | private getRealCursorPosY(): number {
method getVisibleScrollFolders (line 717) | private getVisibleScrollFolders(): CliScanFoundFolder[] {
method paintBgRow (line 724) | private paintBgRow(row: number): void {
method delete (line 739) | private delete(): void {
method getRowsAvailable (line 745) | private getRowsAvailable(): number {
method getRow (line 750) | private getRow(index: number): number {
method showErrorsPopup (line 754) | private showErrorsPopup(): void {
method truncateText (line 758) | private truncateText(text: string, maxLength: number): string {
method clamp (line 793) | private clamp(num: number, min: number, max: number): number {
method results (line 797) | private get results(): CliScanFoundFolder[] {
FILE: src/cli/ui/components/warning.ui.ts
class WarningUi (line 6) | class WarningUi extends BaseUi implements InteractiveUi {
method onKeyInput (line 14) | onKeyInput({ name }: IKeyPress): void {
method setDeleteAllWarningVisibility (line 22) | setDeleteAllWarningVisibility(visible: boolean): void {
method render (line 27) | render(): void {
method printDeleteAllWarning (line 33) | private printDeleteAllWarning(): void {
FILE: src/cli/ui/heavy.ui.ts
method resetBufferState (line 11) | resetBufferState(): void {
method print (line 20) | protected override print(text: string): void {
method flush (line 25) | protected flush(): void {
method clearBuffer (line 39) | private clearBuffer(): void {
FILE: src/constants/cli.constants.ts
constant OPTIONS (line 4) | const OPTIONS: ICliOptions[] = [
constant HELP_HEADER (line 110) | const HELP_HEADER = `Npkill helps you find and manage “junk” directories...
constant HELP_PROGRESSBAR (line 128) | const HELP_PROGRESSBAR = `${getHeader('Header information')}
constant HELP_FOOTER (line 142) | const HELP_FOOTER = `${getHeader('Important note')}
constant COLORS (line 145) | const COLORS = {
FILE: src/constants/main.constants.ts
constant MIN_CLI_COLUMNS_SIZE (line 4) | const MIN_CLI_COLUMNS_SIZE = 60;
constant CURSOR_SIMBOL (line 5) | const CURSOR_SIMBOL = '~>';
constant WIDTH_OVERFLOW (line 6) | const WIDTH_OVERFLOW = '...';
constant DEFAULT_SIZE (line 7) | const DEFAULT_SIZE = '0 MB';
constant DECIMALS_SIZE (line 8) | const DECIMALS_SIZE = 2;
constant OVERFLOW_CUT_FROM (line 9) | const OVERFLOW_CUT_FROM = 11;
constant DEFAULT_CONFIG (line 11) | const DEFAULT_CONFIG: IConfig = {
constant MARGINS (line 29) | const MARGINS = {
constant UI_HELP (line 36) | const UI_HELP = {
constant UI_POSITIONS (line 43) | const UI_POSITIONS = {
constant BANNER (line 81) | const BANNER = ` __ .__.__ .__
constant STREAM_ENCODING (line 88) | const STREAM_ENCODING = 'utf8';
FILE: src/constants/messages.constants.ts
constant MENU_BAR (line 1) | const MENU_BAR = {
constant INFO_MSGS (line 8) | const INFO_MSGS = {
constant ERROR_MSG (line 36) | const ERROR_MSG = {
FILE: src/constants/options.constants.ts
constant OPTIONS_HINTS_BY_TYPE (line 3) | const OPTIONS_HINTS_BY_TYPE = {
FILE: src/constants/result-descriptions.constants.ts
constant RESULT_TYPE_INFO (line 6) | const RESULT_TYPE_INFO = {
FILE: src/constants/sort.result.ts
constant FOLDER_SORT (line 3) | const FOLDER_SORT = {
FILE: src/constants/spinner.constants.ts
constant SPINNER_INTERVAL (line 1) | const SPINNER_INTERVAL = 70;
constant SPINNERS (line 2) | const SPINNERS = {
FILE: src/constants/status.constants.ts
constant BAR_PARTS (line 3) | const BAR_PARTS = {
constant BAR_WIDTH (line 10) | const BAR_WIDTH = 25;
FILE: src/constants/update.constants.ts
constant VERSION_CHECK_DIRECTION (line 1) | const VERSION_CHECK_DIRECTION = 'https://npkill.js.org/version.json';
constant VERSION_KEY (line 2) | const VERSION_KEY = 'last-recomended-version';
FILE: src/constants/workers.constants.ts
constant MAX_WORKERS (line 1) | const MAX_WORKERS = 8;
constant MAX_PROCS (line 4) | const MAX_PROCS = 100;
type EVENTS (line 5) | enum EVENTS {
FILE: src/core/constants/global-ignored.constants.ts
constant GLOBAL_IGNORE (line 7) | const GLOBAL_IGNORE = new Set([
FILE: src/core/constants/profiles.constants.ts
constant DEFAULT_PROFILE (line 4) | const DEFAULT_PROFILE = 'node';
constant BASE_PROFILES (line 6) | const BASE_PROFILES: { [profileName: string]: PROFILE } = {
constant ALL_TARGETS (line 147) | const ALL_TARGETS = [
constant DEFAULT_PROFILES (line 153) | const DEFAULT_PROFILES: { [profileName: string]: PROFILE } = {
FILE: src/core/interfaces/file-service.interface.ts
type IFileService (line 11) | interface IFileService {
type IFileStat (line 84) | interface IFileStat {
FILE: src/core/interfaces/folder.interface.ts
type SizeUnit (line 2) | type SizeUnit = 'bytes';
type SortBy (line 5) | type SortBy = 'path' | 'size' | 'age';
type RiskAnalysis (line 9) | interface RiskAnalysis {
type ScanFoundFolder (line 19) | interface ScanFoundFolder {
type ScanOptions (line 29) | interface ScanOptions {
type GetSizeOptions (line 44) | interface GetSizeOptions {
type GetSizeResult (line 52) | interface GetSizeResult {
type GetNewestFileOptions (line 63) | interface GetNewestFileOptions {}
type GetNewestFileResult (line 68) | interface GetNewestFileResult {
type DeleteOptions (line 80) | interface DeleteOptions {
type DeleteResult (line 88) | interface DeleteResult {
FILE: src/core/interfaces/logger-service.interface.ts
type LogEntry (line 6) | interface LogEntry {
type ILoggerService (line 16) | interface ILoggerService {
FILE: src/core/interfaces/npkill.interface.ts
type ProfileFilterType (line 13) | type ProfileFilterType = 'base' | 'user' | 'all';
type IsValidRootFolderResult (line 18) | interface IsValidRootFolderResult {
type NpkillInterface (line 30) | interface NpkillInterface {
FILE: src/core/interfaces/npkillrc-config.interface.ts
type INpkillrcConfig (line 7) | interface INpkillrcConfig {
type IConfigLoadResult (line 83) | interface IConfigLoadResult {
constant VALID_NPKILLRC_PROPERTIES (line 100) | const VALID_NPKILLRC_PROPERTIES = [
FILE: src/core/interfaces/profile.interface.ts
type PROFILE (line 4) | interface PROFILE {
FILE: src/core/interfaces/search-status.model.ts
class ScanStatus (line 8) | class ScanStatus {
method newResult (line 30) | newResult(): void {
method completeStatCalculation (line 39) | completeStatCalculation(): void {
method reset (line 44) | reset() {
FILE: src/core/interfaces/services.interface.ts
type Services (line 12) | interface Services {
FILE: src/core/npkill.ts
class Npkill (line 34) | class Npkill implements NpkillInterface {
method constructor (line 37) | constructor(customServices?: Partial<Services>) {
method startScan$ (line 49) | startScan$(
method getSize$ (line 88) | getSize$(path: string): Observable<GetSizeResult> {
method getNewestFile$ (line 98) | getNewestFile$(
method delete$ (line 114) | delete$(path: string, options?: DeleteOptions): Observable<DeleteResul...
method getLogs$ (line 138) | getLogs$(): Observable<LogEntry[]> {
method stopScan (line 142) | stopScan(): void {
method isValidRootFolder (line 147) | isValidRootFolder(path: string): IsValidRootFolderResult {
method getVersion (line 151) | getVersion(): string {
method logger (line 158) | get logger(): LoggerService {
function createDefaultServices (line 163) | function createDefaultServices(
function splitData (line 196) | function splitData(data: string, separator = '\n'): string[] {
FILE: src/core/services/config.service.ts
constant DEFAULT_CONFIG_FILENAME (line 11) | const DEFAULT_CONFIG_FILENAME = '.npkillrc';
class ConfigService (line 16) | class ConfigService {
method loadConfig (line 25) | loadConfig(customPath?: string): IConfigLoadResult {
method resolveConfigPath (line 72) | private resolveConfigPath(customPath?: string): string {
method mergeConfigs (line 95) | mergeConfigs<T extends Record<string, unknown>>(
method getUserDefinedProfiles (line 118) | getUserDefinedProfiles(
FILE: src/core/services/config/config-merger.ts
function mergeExcludeArrays (line 6) | function mergeExcludeArrays(
function mergeProperty (line 17) | function mergeProperty<T>(
function isDefined (line 28) | function isDefined<T>(value: T | undefined): value is T {
function applyFileConfigProperties (line 35) | function applyFileConfigProperties(
FILE: src/core/services/config/config-validator.ts
function validateConfig (line 17) | function validateConfig(config: INpkillrcConfig): ValidationResult {
FILE: src/core/services/config/profile-validator.ts
function validateProfile (line 3) | function validateProfile(
function validateProfiles (line 62) | function validateProfiles(value: unknown): ValidationResult {
FILE: src/core/services/config/property-validators.ts
constant VALID_SORT_OPTIONS (line 3) | const VALID_SORT_OPTIONS = ['none', 'size', 'path', 'age'] as const;
constant VALID_SIZE_UNITS (line 4) | const VALID_SIZE_UNITS = ['auto', 'mb', 'gb'] as const;
type ValidationResult (line 6) | interface ValidationResult {
function validateRootDir (line 14) | function validateRootDir(value: unknown): ValidationResult {
function validateExclude (line 39) | function validateExclude(value: unknown): ValidationResult {
function validateSortBy (line 64) | function validateSortBy(value: unknown): ValidationResult {
function validateSizeUnit (line 82) | function validateSizeUnit(value: unknown): ValidationResult {
function validateBoolean (line 100) | function validateBoolean(
function validateDefaultProfiles (line 121) | function validateDefaultProfiles(value: unknown): ValidationResult {
function validateUnknownProperties (line 146) | function validateUnknownProperties(
FILE: src/core/services/files/files.service.ts
method constructor (line 19) | constructor(fileWorkerService: FileWorkerService) {
method listDir (line 25) | listDir(path: string, params: ScanOptions): Observable<string> {
method getFolderSize (line 31) | getFolderSize(path: string): Observable<number> {
method stopScan (line 37) | stopScan(): void {
method fakeDeleteDir (line 42) | async fakeDeleteDir(): Promise<boolean> {
method isValidRootFolder (line 48) | isValidRootFolder(path: string): IsValidRootFolderResult {
method isDangerous (line 85) | isDangerous(originalPath: string): RiskAnalysis {
method getRecentModificationInDir (line 199) | async getRecentModificationInDir(
method getFileStatsInDir (line 218) | async getFileStatsInDir(dirname: string): Promise<IFileStat[]> {
FILE: src/core/services/files/files.worker.service.ts
type WorkerStatus (line 11) | type WorkerStatus = 'stopped' | 'scanning' | 'dead' | 'finished';
type WorkerJob (line 12) | type WorkerJob = {
type WorkerScanOptions (line 17) | interface WorkerScanOptions extends ScanOptions {
type WorkerMessage (line 21) | type WorkerMessage =
type WorkerStats (line 45) | interface WorkerStats {
class FileWorkerService (line 51) | class FileWorkerService {
method constructor (line 67) | constructor(
method startScan (line 72) | async startScan(
method getFolderSize (line 92) | getFolderSize(stream$: Subject<number>, path: string): void {
method stopScan (line 120) | stopScan(): void {
method listenEvents (line 135) | private listenEvents(stream$: Subject<string>): void {
method newWorkerMessage (line 154) | private newWorkerMessage(
method addJob (line 216) | private addJob(job: WorkerJob): void {
method checkJobComplete (line 230) | private checkJobComplete(stream$: Subject<string>): void {
method instantiateWorkers (line 240) | private instantiateWorkers(amount: number): void {
method setWorkerConfig (line 255) | private setWorkerConfig(params: WorkerScanOptions): void {
method killWorkers (line 264) | private async killWorkers(): Promise<void> {
method getPendingJobs (line 288) | private getPendingJobs(): number {
method updateStats (line 292) | private updateStats(): void {
method getWorkerPath (line 298) | private getWorkerPath(): URL {
method getOptimalNumberOfWorkers (line 309) | private getOptimalNumberOfWorkers(): number {
FILE: src/core/services/files/files.worker.ts
type ETaskOperation (line 10) | enum ETaskOperation {
type Task (line 16) | interface Task {
function notifyWorkerReady (line 46) | function notifyWorkerReady(): void {
function initTunnelListeners (line 53) | function initTunnelListeners(): void {
function initFileWalkerListeners (line 77) | function initFileWalkerListeners(): void {
class FileWalker (line 101) | class FileWalker {
method setSearchConfig (line 114) | setSearchConfig(params: WorkerScanOptions): void {
method stop (line 118) | stop(): void {
method enqueueTask (line 122) | enqueueTask(
method run (line 146) | private async run(path: string): Promise<void> {
method analizeDir (line 157) | private async analizeDir(path: string, dir: Dir): Promise<void> {
method runGetFolderSize (line 174) | private async runGetFolderSize(path: string): Promise<void> {
method runGetFolderSizeChild (line 200) | private async runGetFolderSizeChild(
method newDirEntry (line 270) | private newDirEntry(
method isExcluded (line 297) | private isExcluded(path: string): boolean {
method isTargetFolder (line 304) | private isTargetFolder(path: string): boolean {
method completeTask (line 308) | private completeTask(): void {
method updateProcs (line 314) | private updateProcs(value: number): void {
method processQueue (line 318) | private processQueue(): void {
method completeAll (line 365) | private completeAll(): void {
method pendingJobs (line 377) | get pendingJobs(): number {
FILE: src/core/services/files/unix-files.service.ts
class UnixFilesService (line 9) | class UnixFilesService extends FileService {
method constructor (line 10) | constructor(
method deleteDir (line 17) | async deleteDir(path: string): Promise<boolean> {
FILE: src/core/services/files/windows-files.service.ts
class WindowsFilesService (line 8) | class WindowsFilesService extends FileService {
method constructor (line 9) | constructor(
method deleteDir (line 16) | async deleteDir(path: string): Promise<boolean> {
FILE: src/core/services/logger.service.ts
constant LATEST_TAG (line 12) | const LATEST_TAG = 'latest';
constant OLD_TAG (line 13) | const OLD_TAG = 'old';
class LoggerService (line 20) | class LoggerService implements ILoggerService {
method info (line 24) | info(message: string): void {
method warn (line 32) | warn(message: string): void {
method error (line 40) | error(message: string): void {
method get (line 48) | get(type: 'all' | 'info' | 'warn' | 'error' = 'all'): LogEntry[] {
method getLog$ (line 56) | getLog$(): Observable<LogEntry[]> {
method getLogByType$ (line 60) | getLogByType$(
method saveToFile (line 74) | saveToFile(path: string): void {
method getSuggestLogFilePath (line 88) | getSuggestLogFilePath(): string {
method rotateLogFile (line 93) | private rotateLogFile(newLogPath: string): void {
method addToLog (line 104) | private addToLog(entry: LogEntry): void {
method getTimestamp (line 109) | private getTimestamp(): number {
FILE: src/core/services/profiles.service.ts
type ProfileFilterType (line 4) | type ProfileFilterType = 'base' | 'user' | 'all';
class ProfilesService (line 10) | class ProfilesService {
method setUserDefinedProfiles (line 17) | setUserDefinedProfiles(profiles: Record<string, PROFILE>): void {
method getProfiles (line 29) | getProfiles(filterType: ProfileFilterType = 'all'): Record<string, PRO...
method getProfileByName (line 48) | getProfileByName(name: string): PROFILE | undefined {
method hasProfile (line 57) | hasProfile(name: string): boolean {
method getTargetsFromProfiles (line 67) | getTargetsFromProfiles(profileNames: string[]): string[] {
method getInvalidProfileNames (line 87) | getInvalidProfileNames(profileNames: string[]): string[] {
method getDefaultProfileName (line 95) | getDefaultProfileName(): string {
FILE: src/core/services/stream.service.ts
class StreamService (line 10) | class StreamService {
method streamToObservable (line 11) | streamToObservable<T>(stream: ChildProcessWithoutNullStreams): Observa...
method getStream (line 39) | getStream<T>(child: ChildProcessWithoutNullStreams): Observable<T> {
method setEncoding (line 44) | private setEncoding(
FILE: src/utils/get-file-content.ts
function getFileContent (line 3) | function getFileContent(path: string): string {
FILE: src/utils/is-safe-to-delete.ts
function isSafeToDelete (line 3) | function isSafeToDelete(filePath: string, targets: string[]): boolean {
FILE: src/utils/unit-conversions.ts
function convertBytesToKB (line 1) | function convertBytesToKB(bytes: number): number {
function convertBytesToGb (line 6) | function convertBytesToGb(bytes: number): number {
function convertGBToMB (line 10) | function convertGBToMB(gb: number): number {
function convertGbToKb (line 15) | function convertGbToKb(gb: number): number {
function convertGbToBytes (line 20) | function convertGbToBytes(gb: number): number {
type FormattedSize (line 24) | interface FormattedSize {
function formatSize (line 30) | function formatSize(
FILE: tests/cli/cli.controller.test.ts
method setVisible (line 82) | setVisible() {}
class CliController (line 92) | class CliController extends CliControllerConstructor {}
FILE: tests/cli/services/https.service.test.ts
class HttpsService (line 29) | class HttpsService extends HttpsServiceConstructor {}
FILE: tests/cli/ui/results.ui.test.ts
class ResultsUi (line 25) | class ResultsUi extends ResultsUiConstructor {}
FILE: tests/core/services/files/files.service.test.ts
class UnixFilesService (line 33) | class UnixFilesService extends UnixFilesServiceConstructor {}
class WindowsFilesService (line 38) | class WindowsFilesService extends WindowsFilesServiceConstructor {}
FILE: tests/core/services/files/files.worker.service.test.ts
class FileWorkerService (line 58) | class FileWorkerService extends FileWorkerServiceConstructor {}
FILE: tests/core/services/logger.service.test.ts
class LoggerService (line 26) | class LoggerService extends LoggerServiceConstructor {}
FILE: tests/index.test.ts
function importIndex (line 40) | function importIndex() {
Condensed preview — 140 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (536K chars).
[
{
"path": ".github/CODE_OF_CONDUCT.es.md",
"chars": 3540,
"preview": "# Código de Conducta\n\n## Nuestro Compromiso\n\nEn el interés de fomentar un entorno abierto y acogedor, nosotros como cola"
},
{
"path": ".github/CODE_OF_CONDUCT.md",
"chars": 3380,
"preview": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nIn the interest of fostering an open and welcoming environment, w"
},
{
"path": ".github/CONTRIBUTING.es.md",
"chars": 3883,
"preview": "**_(Este doc está en proceso de desarrollo)_**\n\n# Cómo contribuir a NPKILL 🎉\n\nSé que lo que voy a decir es lo típico, pe"
},
{
"path": ".github/CONTRIBUTING.md",
"chars": 3711,
"preview": "**_(this doc is under construction)_**\n\n# How to contribute on NPKILL 🎉\n\nI know that what I am going to say sounds like "
},
{
"path": ".github/FUNDING.yml",
"chars": 135,
"preview": "# These are supported funding model platforms\n\nopen_collective: npkill\ncustom: ['ethereum/0x7668e86c8bdb52034606db5aa0d2"
},
{
"path": ".github/ISSUE_TEMPLATE/bug-report.md",
"chars": 554,
"preview": "---\nname: Bug report\nabout: Create a report to help us improve\ntitle: ''\nlabels: ''\nassignees: ''\n---\n\n**Describe the bu"
},
{
"path": ".github/ISSUE_TEMPLATE/feature_request.md",
"chars": 594,
"preview": "---\nname: Feature request\nabout: Suggest an idea for this project\ntitle: ''\nlabels: ''\nassignees: ''\n---\n\n**Is your feat"
},
{
"path": ".github/workflows/codeql-analysis.yml",
"chars": 2169,
"preview": "# For most projects, this workflow file will not need changing; you simply need\n# to commit it to your repository.\n#\n# Y"
},
{
"path": ".github/workflows/nodejs.yml",
"chars": 906,
"preview": "# This workflow will do a clean install of node dependencies, build the source code and run tests across different versi"
},
{
"path": ".gitignore",
"chars": 127,
"preview": "node_modules\nlib\nout.txt\n# stryker temp files\n.stryker-tmp\nstryker.log\nreports\ncoverage\nstuff\ntest-files\ndocs/private\n.n"
},
{
"path": ".husky/commit-msg",
"chars": 31,
"preview": "npx --no -- commitlint --edit \n"
},
{
"path": ".husky/pre-commit",
"chars": 16,
"preview": "npx lint-staged\n"
},
{
"path": ".node-version",
"chars": 7,
"preview": "20.12.0"
},
{
"path": ".npmignore",
"chars": 41,
"preview": "src\ntsconfig.json\ntslint.json\n.prettierrc"
},
{
"path": ".prettierrc",
"chars": 85,
"preview": "{\n \"trailingComma\": \"all\",\n \"tabWidth\": 2,\n \"semi\": true,\n \"singleQuote\": true\n}\n"
},
{
"path": ".vscode/launch.json",
"chars": 435,
"preview": "{\n // Use IntelliSense to learn about possible attributes.\n // Hover to view descriptions of existing attributes.\n //"
},
{
"path": ".vscode/settings.json",
"chars": 89,
"preview": "{\n \"editor.formatOnSave\": true,\n \"debug.javascript.autoAttachFilter\": \"onlyWithFlag\"\n}\n"
},
{
"path": "API.md",
"chars": 6577,
"preview": "# NPKill API\n\nThis document does not include all project documentation at this stage. It brings together the basic conce"
},
{
"path": "LICENSE",
"chars": 1104,
"preview": "MIT License\n\nCopyright (c) 2025 Estefanía García Gallardo and Juan Torres Gómez\n\nPermission is hereby granted, free of c"
},
{
"path": "README.es.md",
"chars": 11036,
"preview": "<p align=\"center\">\n <img src=\"https://npkill.js.org/img/npkill-text-outlined.svg\" width=\"320\" alt=\"npkill logo\" />\n <i"
},
{
"path": "README.id.md",
"chars": 9440,
"preview": "<p align=\"center\">\n <img src=\"https://npkill.js.org/img/npkill-text-outlined.svg\" width=\"320\" alt=\"npkill logo\" />\n <i"
},
{
"path": "README.md",
"chars": 13704,
"preview": "<p align=\"center\">\n <img src=\"./docs/npkill-text-clean.svg\" width=\"380\" alt=\"npkill logo\" />\n</p>\n<p align=\"center\">\n<i"
},
{
"path": "README.pt.md",
"chars": 10903,
"preview": "<p align=\"center\">\n <img src=\"https://npkill.js.org/img/npkill-text-outlined.svg\" width=\"320\" alt=\"npkill logo\" />\n <i"
},
{
"path": "README.tr.md",
"chars": 10585,
"preview": "<p align=\"center\">\n <img src=\"https://npkill.js.org/img/npkill-text-outlined.svg\" width=\"320\" alt=\"npkill logo\" />\n <i"
},
{
"path": "docs/RELEASE.md",
"chars": 591,
"preview": "# How to release a new version\n\n### 1. Ensure the latest changes are available\n\n```bash\ngit checkout develop\ngit pull\ngi"
},
{
"path": "docs/create-demo.sh",
"chars": 1226,
"preview": "#!/bin/bash\n#\n# This script create a example node_modules files\n# only for demo purpose.\n#\n\nBASE_PATH=\"$HOME/allStartHer"
},
{
"path": "docs/json-output.md",
"chars": 5498,
"preview": "# JSON Output\n\nNpkill supports two JSON output modes that allow you to integrate it into automation scripts, monitoring "
},
{
"path": "docs/npkillrc.md",
"chars": 4611,
"preview": "# Config File - npkillrc\n\nYou can customize the behavior of npkill through the config file (`.npkillrc` by default).\n\n##"
},
{
"path": "docs/profiles.md",
"chars": 12919,
"preview": "# Profiles\n\nThis document defines built-in profiles for npkill. A profile is a named preset of \"safe-to-delete\" director"
},
{
"path": "eslint.config.mjs",
"chars": 649,
"preview": "// @ts-check\n\nimport eslint from '@eslint/js';\nimport tseslint from 'typescript-eslint';\nimport nodePlugin from 'eslint-"
},
{
"path": "jest.config.ts",
"chars": 872,
"preview": "import type { JestConfigWithTsJest } from 'ts-jest';\n\nconst config: JestConfigWithTsJest = {\n preset: 'ts-jest/presets/"
},
{
"path": "package.json",
"chars": 2538,
"preview": "{\n \"name\": \"npkill\",\n \"version\": \"0.12.2\",\n \"description\": \"List any node_modules directories in your system, as well"
},
{
"path": "src/cli/cli.controller.ts",
"chars": 28281,
"preview": "import {\n ConsoleService,\n ResultsService,\n SpinnerService,\n UpdateService,\n} from './services/index.js';\nimport {\n "
},
{
"path": "src/cli/interfaces/cli-options.interface.ts",
"chars": 89,
"preview": "export interface ICliOptions {\n arg: string[];\n name: string;\n description: string;\n}\n"
},
{
"path": "src/cli/interfaces/command-keys.interface.ts",
"chars": 356,
"preview": "export interface IKeysCommand {\n up: () => void;\n down: () => void;\n space: () => void;\n j: () => void;\n k: () => v"
},
{
"path": "src/cli/interfaces/config.interface.ts",
"chars": 387,
"preview": "export interface IConfig {\n profiles: string[];\n folderRoot: string;\n checkUpdates: boolean;\n deleteAll: boolean;\n "
},
{
"path": "src/cli/interfaces/index.ts",
"chars": 496,
"preview": "export * from './cli-options.interface.js';\nexport * from './command-keys.interface.js';\nexport * from './config.interfa"
},
{
"path": "src/cli/interfaces/json-output.interface.ts",
"chars": 922,
"preview": "import { CliScanFoundFolder } from './stats.interface.js';\n\nexport interface JsonOutputBase {\n version: number;\n}\n\nexpo"
},
{
"path": "src/cli/interfaces/key-press.interface.ts",
"chars": 119,
"preview": "export interface IKeyPress {\n name: string;\n meta: boolean;\n ctrl: boolean;\n shift: boolean;\n sequence: string;\n}\n"
},
{
"path": "src/cli/interfaces/node-version.interface.ts",
"chars": 85,
"preview": "export interface INodeVersion {\n major: number;\n minor: number;\n patch: number;\n}\n"
},
{
"path": "src/cli/interfaces/stats.interface.ts",
"chars": 426,
"preview": "import { ScanFoundFolder } from '../../core/interfaces/index.js';\n\nexport interface CliScanFoundFolder extends ScanFound"
},
{
"path": "src/cli/interfaces/ui-positions.interface.ts",
"chars": 111,
"preview": "export interface IPosition {\n x: number;\n y: number;\n}\n\nexport type IUiPosition = Record<string, IPosition>;\n"
},
{
"path": "src/cli/interfaces/version.interface.ts",
"chars": 81,
"preview": "export interface IVersion {\n major: number;\n minor: number;\n patch: number;\n}\n"
},
{
"path": "src/cli/models/start-parameters.model.ts",
"chars": 701,
"preview": "export class StartParameters {\n private values: Record<string, string | boolean> = {};\n\n add(key: string, value: strin"
},
{
"path": "src/cli/services/console.service.ts",
"chars": 3456,
"preview": "import { OPTIONS, WIDTH_OVERFLOW } from '../../constants/index.js';\n\nimport { ICliOptions } from '../interfaces/cli-opti"
},
{
"path": "src/cli/services/https.service.ts",
"chars": 983,
"preview": "import * as https from 'node:https';\n\nexport class HttpsService {\n async getJson(url: string): Promise<Record<string, s"
},
{
"path": "src/cli/services/index.ts",
"chars": 284,
"preview": "export * from './console.service.js';\nexport * from './https.service.js';\nexport * from './results.service.js';\nexport *"
},
{
"path": "src/cli/services/json-output.service.ts",
"chars": 3321,
"preview": "import { CliScanFoundFolder } from '../interfaces/stats.interface.js';\nimport {\n JsonStreamOutput,\n JsonSimpleOutput,\n"
},
{
"path": "src/cli/services/results.service.ts",
"chars": 1706,
"preview": "import {\n CliScanFoundFolder,\n IStats,\n IResultTypeCount,\n} from '../interfaces/index.js';\nimport { FOLDER_SORT } fro"
},
{
"path": "src/cli/services/scan.service.ts",
"chars": 3957,
"preview": "import { Npkill } from '@core/npkill';\nimport {\n CliScanFoundFolder,\n IConfig,\n ScanFoundFolder,\n ScanOptions,\n Sor"
},
{
"path": "src/cli/services/spinner.service.ts",
"chars": 532,
"preview": "export class SpinnerService {\n private spinner: string[] = [];\n private count = -1;\n\n setSpinner(spinner: string[]): "
},
{
"path": "src/cli/services/ui.service.ts",
"chars": 1651,
"preview": "import ansiEscapes from 'ansi-escapes';\nimport { Position, BaseUi } from '../ui/index.js';\n\nexport class UiService {\n s"
},
{
"path": "src/cli/services/update.service.ts",
"chars": 1670,
"preview": "import {\n VERSION_CHECK_DIRECTION,\n VERSION_KEY,\n} from '../../constants/update.constants.js';\n\nimport { HttpsService "
},
{
"path": "src/cli/ui/base.ui.ts",
"chars": 1541,
"preview": "import { IKeyPress } from '../interfaces/index.js';\nimport ansiEscapes from 'ansi-escapes';\n\nexport interface Position {"
},
{
"path": "src/cli/ui/components/general.ui.ts",
"chars": 508,
"preview": "// This class in only a intermediate for the refactor.\n\nimport { BaseUi } from '../base.ui.js';\nimport pc from 'picocolo"
},
{
"path": "src/cli/ui/components/header/header-ui.constants.ts",
"chars": 86,
"preview": "export enum MENU_BAR_OPTIONS {\n HELP = 0,\n OPTIONS = 1,\n DELETE = 2,\n INFO = 3,\n}\n"
},
{
"path": "src/cli/ui/components/header/header.ui.ts",
"chars": 3339,
"preview": "import { BehaviorSubject } from 'rxjs';\nimport {\n BANNER,\n UI_POSITIONS,\n MENU_BAR,\n INFO_MSGS,\n} from '../../../../"
},
{
"path": "src/cli/ui/components/header/stats.ui.ts",
"chars": 8143,
"preview": "import { UI_POSITIONS, INFO_MSGS } from '../../../../constants/index.js';\nimport { BaseUi } from '../../base.ui.js';\nimp"
},
{
"path": "src/cli/ui/components/header/status.ui.ts",
"chars": 6957,
"preview": "import { BaseUi } from '../../base.ui.js';\nimport pc from 'picocolors';\nimport { SpinnerService } from '../../../service"
},
{
"path": "src/cli/ui/components/help/help-command.ui.ts",
"chars": 2769,
"preview": "import {\n HELP_HEADER,\n OPTIONS,\n HELP_FOOTER,\n HELP_PROGRESSBAR,\n} from '../../../../constants/cli.constants.js';\ni"
},
{
"path": "src/cli/ui/components/help/help.constants.ts",
"chars": 9416,
"preview": "/* eslint-disable quotes */\nimport pc from 'picocolors';\n\ninterface HelpSection {\n id: string;\n title: string;\n icon:"
},
{
"path": "src/cli/ui/components/help/help.ui.ts",
"chars": 6437,
"preview": "import { MARGINS } from '../../../../constants/main.constants.js';\nimport { BaseUi, InteractiveUi } from '../../base.ui."
},
{
"path": "src/cli/ui/components/logs.ui.ts",
"chars": 4308,
"preview": "import { LoggerService } from '@core/services/logger.service.js';\nimport { InteractiveUi, BaseUi } from '../base.ui.js';"
},
{
"path": "src/cli/ui/components/options.ui.ts",
"chars": 10847,
"preview": "import { MARGINS } from '../../../constants/main.constants.js';\nimport { BaseUi, InteractiveUi } from '../base.ui.js';\ni"
},
{
"path": "src/cli/ui/components/result-details.ui.ts",
"chars": 6548,
"preview": "import { MARGINS } from '../../../constants/main.constants.js';\nimport { BaseUi, InteractiveUi } from '../base.ui.js';\ni"
},
{
"path": "src/cli/ui/components/results.ui.ts",
"chars": 21451,
"preview": "import {\n DECIMALS_SIZE,\n DEFAULT_CONFIG,\n MARGINS,\n OVERFLOW_CUT_FROM,\n} from '../../../constants/main.constants.js"
},
{
"path": "src/cli/ui/components/warning.ui.ts",
"chars": 944,
"preview": "import { InteractiveUi, BaseUi } from '../base.ui.js';\nimport { Subject } from 'rxjs';\nimport { IKeyPress } from '../../"
},
{
"path": "src/cli/ui/heavy.ui.ts",
"chars": 942,
"preview": "import { BaseUi } from './base.ui.js';\n\n/**\n * A UI that buffers the output and prints it all at once when calling the\n "
},
{
"path": "src/cli/ui/index.ts",
"chars": 577,
"preview": "export * from './base.ui.js';\nexport * from './heavy.ui.js';\nexport * from './components/general.ui.js';\nexport * from '"
},
{
"path": "src/constants/cli.constants.ts",
"chars": 5600,
"preview": "import { ICliOptions } from '../cli/interfaces/index.js';\nimport pc from 'picocolors';\n\nexport const OPTIONS: ICliOption"
},
{
"path": "src/constants/index.ts",
"chars": 319,
"preview": "export * from './cli.constants.js';\nexport * from './main.constants.js';\nexport * from './messages.constants.js';\nexport"
},
{
"path": "src/constants/main.constants.ts",
"chars": 2400,
"preview": "import { DEFAULT_PROFILE } from '../core/constants/index.js';\nimport { IConfig } from '../cli/interfaces/index.js';\n\nexp"
},
{
"path": "src/constants/messages.constants.ts",
"chars": 1577,
"preview": "export const MENU_BAR = {\n HELP: 'Help',\n OPTIONS: 'Options',\n DELETE: 'Delete',\n INFO: 'Info',\n};\n\nexport const INF"
},
{
"path": "src/constants/options.constants.ts",
"chars": 571,
"preview": "import pc from 'picocolors';\n\nexport const OPTIONS_HINTS_BY_TYPE = {\n input: pc.gray(\n `${pc.bold(pc.underline('SPAC"
},
{
"path": "src/constants/os-service-map.constants.ts",
"chars": 398,
"preview": "import {\n UnixFilesService,\n WindowsFilesService,\n} from '../core/services/files/index.js';\n\n/**\n * A mapping of opera"
},
{
"path": "src/constants/result-descriptions.constants.ts",
"chars": 17782,
"preview": "/* eslint-disable quotes */\n///////////\n// IMPORTANT: Keys must be lowercase to match lookup logic\n///////////\n\nexport c"
},
{
"path": "src/constants/sort.result.ts",
"chars": 760,
"preview": "import { CliScanFoundFolder } from '../cli/interfaces/index.js';\n\nexport const FOLDER_SORT = {\n path: (a: CliScanFoundF"
},
{
"path": "src/constants/spinner.constants.ts",
"chars": 872,
"preview": "export const SPINNER_INTERVAL = 70;\nexport const SPINNERS = {\n SPRING: [\n '⠈',\n '⠉',\n '⠋',\n '⠓',\n '⠒',\n "
},
{
"path": "src/constants/status.constants.ts",
"chars": 200,
"preview": "import pc from 'picocolors';\n\nexport const BAR_PARTS = {\n bg: pc.gray('▀'),\n searchTask: pc.white('▀'),\n calculatingT"
},
{
"path": "src/constants/update.constants.ts",
"chars": 131,
"preview": "export const VERSION_CHECK_DIRECTION = 'https://npkill.js.org/version.json';\nexport const VERSION_KEY = 'last-recomended"
},
{
"path": "src/constants/workers.constants.ts",
"chars": 434,
"preview": "export const MAX_WORKERS = 8;\n// More PROCS improve the speed of the search in the worker,\n// but it will greatly increa"
},
{
"path": "src/core/constants/global-ignored.constants.ts",
"chars": 764,
"preview": "/*\nThese directories will always be excluded during the search.\nHowever, if the name matches a target, it will be displa"
},
{
"path": "src/core/constants/index.ts",
"chars": 41,
"preview": "export * from './profiles.constants.js';\n"
},
{
"path": "src/core/constants/profiles.constants.ts",
"chars": 4612,
"preview": "/* eslint-disable quotes */\nimport { PROFILE } from '../interfaces/profile.interface.js';\n\nexport const DEFAULT_PROFILE "
},
{
"path": "src/core/index.ts",
"chars": 143,
"preview": "export * from './npkill.js';\nexport * from './interfaces/index.js';\nexport * from './services/index.js';\nexport * from '"
},
{
"path": "src/core/interfaces/file-service.interface.ts",
"chars": 3007,
"preview": "import { FileWorkerService } from '../services/files/files.worker.service.js';\nimport { GetNewestFileResult, RiskAnalysi"
},
{
"path": "src/core/interfaces/folder.interface.ts",
"chars": 2739,
"preview": "/** Unit for representing file/directory sizes. */\nexport type SizeUnit = 'bytes'; // | 'kb' | 'mb' | 'gb'; // TODO impl"
},
{
"path": "src/core/interfaces/index.ts",
"chars": 255,
"preview": "export * from './file-service.interface.js';\nexport * from './folder.interface.js';\nexport * from './services.interface."
},
{
"path": "src/core/interfaces/logger-service.interface.ts",
"chars": 1932,
"preview": "import { Observable } from 'rxjs';\n\n/**\n * Represents an individual entry in the log.\n */\nexport interface LogEntry {\n "
},
{
"path": "src/core/interfaces/npkill.interface.ts",
"chars": 3191,
"preview": "import { Observable } from 'rxjs';\nimport {\n ScanFoundFolder,\n GetNewestFileResult,\n GetSizeOptions,\n GetSizeResult,"
},
{
"path": "src/core/interfaces/npkillrc-config.interface.ts",
"chars": 2525,
"preview": "import { PROFILE } from './profile.interface.js';\n\n/**\n * Represents the structure of .npkillrc configuration file.\n * A"
},
{
"path": "src/core/interfaces/profile.interface.ts",
"chars": 243,
"preview": "/**\n * Represents a profile with target directories and description.\n */\nexport interface PROFILE {\n /** Array of direc"
},
{
"path": "src/core/interfaces/search-status.model.ts",
"chars": 1765,
"preview": "import { WorkerStatus } from '../services/files/files.worker.service.js';\n\n/**\n * Tracks the progress and status of dire"
},
{
"path": "src/core/interfaces/services.interface.ts",
"chars": 1215,
"preview": "import { FileService, FileWorkerService } from '@core/services/files/index.js';\nimport { LoggerService } from '@core/ser"
},
{
"path": "src/core/npkill.ts",
"chars": 6068,
"preview": "import { FileWorkerService } from './services/files/index.js';\nimport { from, Observable } from 'rxjs';\nimport { catchEr"
},
{
"path": "src/core/services/config/config-merger.ts",
"chars": 2075,
"preview": "import { INpkillrcConfig } from '../../interfaces/npkillrc-config.interface.js';\n\n/**\n * Merges exclude arrays from base"
},
{
"path": "src/core/services/config/config-validator.ts",
"chars": 1941,
"preview": "import {\n INpkillrcConfig,\n VALID_NPKILLRC_PROPERTIES,\n} from '../../interfaces/npkillrc-config.interface.js';\nimport "
},
{
"path": "src/core/services/config/index.ts",
"chars": 471,
"preview": "export { validateConfig } from './config-validator.js';\nexport {\n validateRootDir,\n validateExclude,\n validateSortBy,"
},
{
"path": "src/core/services/config/profile-validator.ts",
"chars": 2058,
"preview": "import { ValidationResult } from './property-validators.js';\n\nexport function validateProfile(\n profileName: string,\n "
},
{
"path": "src/core/services/config/property-validators.ts",
"chars": 3507,
"preview": "import { INpkillrcConfig } from '../../interfaces/npkillrc-config.interface.js';\n\nconst VALID_SORT_OPTIONS = ['none', 's"
},
{
"path": "src/core/services/config.service.ts",
"chars": 3584,
"preview": "import { existsSync, readFileSync } from 'fs';\nimport { homedir } from 'os';\nimport { join } from 'path';\nimport {\n ICo"
},
{
"path": "src/core/services/files/files.service.ts",
"chars": 8052,
"preview": "import path from 'path';\nimport os from 'os';\nimport {\n ScanOptions,\n IFileService,\n IFileStat,\n GetNewestFileResult"
},
{
"path": "src/core/services/files/files.worker.service.ts",
"chars": 9186,
"preview": "import os from 'os';\nimport { dirname, extname } from 'path';\n\nimport { Worker, MessageChannel, MessagePort } from 'work"
},
{
"path": "src/core/services/files/files.worker.ts",
"chars": 9358,
"preview": "import { Dir, Dirent } from 'fs';\nimport { lstat, opendir, readdir } from 'fs/promises';\nimport EventEmitter from 'event"
},
{
"path": "src/core/services/files/index.ts",
"chars": 164,
"preview": "export * from './files.service.js';\nexport * from './files.worker.service.js';\nexport * from './unix-files.service.js';\n"
},
{
"path": "src/core/services/files/unix-files.service.ts",
"chars": 909,
"preview": "import { exec } from 'child_process';\n\nimport { FileService } from './files.service.js';\nimport { Observable, Subject } "
},
{
"path": "src/core/services/files/windows-files.service.ts",
"chars": 641,
"preview": "import { Subject, Observable } from 'rxjs';\nimport { FileService } from './files.service.js';\nimport { FileWorkerService"
},
{
"path": "src/core/services/index.ts",
"chars": 184,
"preview": "export * from './logger.service.js';\nexport * from './stream.service.js';\nexport * from './config.service.js';\nexport * "
},
{
"path": "src/core/services/logger.service.ts",
"chars": 2821,
"preview": "import { tmpdir } from 'os';\nimport { existsSync, renameSync, writeFileSync } from 'fs';\nimport { basename, dirname, joi"
},
{
"path": "src/core/services/profiles.service.ts",
"chars": 3080,
"preview": "import { DEFAULT_PROFILES } from '../constants/profiles.constants.js';\nimport { PROFILE } from '../interfaces/profile.in"
},
{
"path": "src/core/services/stream.service.ts",
"chars": 1781,
"preview": "import { ChildProcessWithoutNullStreams } from 'child_process';\nimport { Observable } from 'rxjs';\nimport { STREAM_ENCOD"
},
{
"path": "src/dirname.ts",
"chars": 183,
"preview": "import { fileURLToPath } from 'url';\nimport { dirname } from 'path';\n\nconst _filename = fileURLToPath(import.meta.url);\n"
},
{
"path": "src/index.ts",
"chars": 390,
"preview": "#!/usr/bin/env node\n\nimport { fileURLToPath } from 'url';\nimport main from './main.js';\n\n// Check if npkill is called di"
},
{
"path": "src/main.ts",
"chars": 1250,
"preview": "import {\n ConsoleService,\n HttpsService,\n JsonOutputService,\n ResultsService,\n SpinnerService,\n UpdateService,\n} f"
},
{
"path": "src/utils/get-file-content.ts",
"chars": 159,
"preview": "import { readFileSync } from 'fs';\n\nexport function getFileContent(path: string): string {\n const encoding = 'utf8';\n "
},
{
"path": "src/utils/is-safe-to-delete.ts",
"chars": 254,
"preview": "import * as path from 'path';\n\nexport function isSafeToDelete(filePath: string, targets: string[]): boolean {\n const la"
},
{
"path": "src/utils/unit-conversions.ts",
"chars": 1481,
"preview": "export function convertBytesToKB(bytes: number): number {\n const factorBytestoKB = 1024;\n return bytes / factorBytesto"
},
{
"path": "stryker.conf.js",
"chars": 397,
"preview": "/**\n * @type {import('@stryker-mutator/api/core').StrykerOptions}\n */\nconst config = {\n packageManager: 'npm',\n report"
},
{
"path": "tests/cli/cli.controller.test.ts",
"chars": 14056,
"preview": "import { jest } from '@jest/globals';\nimport { StartParameters } from '../../src/cli/models/start-parameters.model.js';\n"
},
{
"path": "tests/cli/services/console.service.test.ts",
"chars": 5688,
"preview": "import { ConsoleService } from '../../../src/cli/services/console.service.js';\n\ndescribe('Console Service', () => {\n le"
},
{
"path": "tests/cli/services/https.service.test.ts",
"chars": 2384,
"preview": "/* eslint-disable promise/valid-params */\n/* eslint-disable promise/catch-or-return */\n/* eslint-disable promise/no-call"
},
{
"path": "tests/cli/services/json-output.service.test.ts",
"chars": 6379,
"preview": "import { jest } from '@jest/globals';\nimport { JsonOutputService } from '../../../src/cli/services/json-output.service.j"
},
{
"path": "tests/cli/services/profiles.service.test.ts",
"chars": 14488,
"preview": "import { ProfilesService } from '../../../src/core/services/profiles.service.js';\nimport { DEFAULT_PROFILES } from '../."
},
{
"path": "tests/cli/services/result.service.test.ts",
"chars": 6418,
"preview": "import { CliScanFoundFolder } from '../../../src/cli/interfaces/stats.interface.js';\nimport { ResultsService } from '../"
},
{
"path": "tests/cli/services/scan.service.test.ts",
"chars": 7621,
"preview": "import { jest } from '@jest/globals';\nimport { ScanService } from '../../../src/cli/services/scan.service.js';\nimport { "
},
{
"path": "tests/cli/services/spinner.service.test.ts",
"chars": 1257,
"preview": "import { jest } from '@jest/globals';\nimport { SpinnerService } from '../../../src/cli/services/spinner.service.js';\n\nde"
},
{
"path": "tests/cli/services/ui.service.test.ts",
"chars": 1534,
"preview": "import { jest } from '@jest/globals';\nimport { UiService } from '../../../src/cli/services/ui.service.js';\n\njest.mock('."
},
{
"path": "tests/cli/services/update.service.test.ts",
"chars": 2882,
"preview": "/* eslint-disable promise/no-callback-in-promise */\n/* eslint-disable promise/always-return */\nimport { jest } from '@je"
},
{
"path": "tests/cli/ui/results.ui.test.ts",
"chars": 7705,
"preview": "import { ConsoleService } from '../../../src/cli/services/index.js';\nimport { ResultsService } from '../../../src/cli/se"
},
{
"path": "tests/core/npkill.test.ts",
"chars": 16065,
"preview": "import { jest } from '@jest/globals';\nimport { of, throwError, BehaviorSubject } from 'rxjs';\nimport { take } from 'rxjs"
},
{
"path": "tests/core/services/config.service.test.ts",
"chars": 18991,
"preview": "import { ConfigService } from '../../../src/core/services/config.service.js';\nimport { INpkillrcConfig } from '../../../"
},
{
"path": "tests/core/services/files/files.service.test.ts",
"chars": 9823,
"preview": "import { jest } from '@jest/globals';\n\nimport { IFileService } from '../../../../src/core/interfaces/file-service.interf"
},
{
"path": "tests/core/services/files/files.worker.service.test.ts",
"chars": 7973,
"preview": "import { jest } from '@jest/globals';\nimport EventEmitter from 'node:events';\nimport { Subject } from 'rxjs';\nimport { E"
},
{
"path": "tests/core/services/files/files.worker.test.ts",
"chars": 14561,
"preview": "import { jest } from '@jest/globals';\nimport EventEmitter from 'node:events';\nimport { Dir } from 'node:fs';\nimport { jo"
},
{
"path": "tests/core/services/logger.service.test.ts",
"chars": 4025,
"preview": "import { jest } from '@jest/globals';\nimport { normalize } from 'path';\n\nconst writeFileSyncMock = jest.fn();\nconst rena"
},
{
"path": "tests/index.test.ts",
"chars": 1016,
"preview": "import { jest } from '@jest/globals';\n\nconst mainMock = jest.fn();\nconst fileURLToPathMock = jest.fn();\n\njest.unstable_m"
},
{
"path": "tests/main.test.ts",
"chars": 2450,
"preview": "import { jest } from '@jest/globals';\n\nconst controllerConstructorMock = jest.fn();\nconst constructorInitMock = jest.fn("
},
{
"path": "tests/utils/utils.test.ts",
"chars": 3676,
"preview": "import {\n convertBytesToKB,\n convertBytesToGb,\n convertGBToMB,\n formatSize,\n} from '../../src/utils/unit-conversions"
},
{
"path": "tsconfig.json",
"chars": 661,
"preview": "{\n \"compilerOptions\": {\n \"esModuleInterop\": true,\n \"sourceMap\": true,\n \"module\": \"ESNext\",\n \"target\": \"ESNe"
},
{
"path": "tslint.json",
"chars": 349,
"preview": "{\n \"defaultSeverity\": \"error\",\n \"extends\": [\"tslint:recommended\"],\n \"jsRules\": {},\n \"rules\": {\n \"quotemark\": [tru"
}
]
About this extraction
This page contains the full source code of the voidcosmos/npkill GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 140 files (497.9 KB), approximately 126.2k tokens, and a symbol index with 511 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.