npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

jagg-cli

v0.4.0

Published

CLI de scaffolding con Clean Architecture, adapters y singletons + docker-compose

Downloads

1,048

Readme

jagg-cli

CLI de scaffolding para proyectos Node.js + TypeScript con Clean Architecture. Con un comando genera los archivos de un servicio (contratos de dominio, adapters, singletons), instala sus dependencias, lo agrega al docker-compose.yml y completa el .env.example y el .env (los crea si no existen y nunca pisa valores que ya tengas). Todo se puede deshacer.

npm install -g jagg-cli

jg g clean      # estructura de carpetas
jg g express    # servidor Express 5 + TypeScript listo para usar
jg g env        # validador de variables de entorno (Zod)
jg g redis      # cache Redis + servicio docker
jg g prisma     # ORM Prisma + Postgres en docker
jg g user-crud  # CRUD de usuarios completo en /api/users
jg g datasource user --orm mongoose   # cambia de base de datos/ORM sin tocar el dominio
jg g metrics    # monitoreo con Prometheus + Grafana, dashboard incluido
jg undo         # deshace lo último

Requisitos

  • Node.js 20.12 o superior
  • Un proyecto con package.json (npm, pnpm, yarn o bun; se detecta por el lockfile)

Empezar un proyecto desde cero

mkdir mi-api && cd mi-api
npm init -y              # jg necesita un package.json
jg g clean
jg g express
jg g user-crud
npm run dev              # http://localhost:3000/api/users

No hace falta ejecutar npm install a mano: cada generador instala sus propias dependencias con el gestor de paquetes del proyecto (por ejemplo, jg g express instala express, typescript, tsx...). Si usas --skip-install, instálalas tú después con npm install <paquetes> (la columna "Dependencias" de la tabla de generadores dice cuáles). Al clonar un proyecto ya generado, basta el npm install habitual.

Comandos

| Comando | Descripción | |---|---| | jg g <generador> [nombre] | Ejecuta un generador (g es alias de generate) | | jg list | Lista los generadores disponibles | | jg undo [n] | Deshace las últimas n generaciones (por defecto 1) | | jg history | Muestra lo que se puede deshacer | | jg init | Crea jg.config.json |

Opciones de generate

| Opción | Efecto | |---|---| | -d, --dry-run | Muestra lo que haría sin escribir nada | | -f, --force | Sobrescribe archivos y servicios existentes | | --skip-install | No instala dependencias | | --skip-docker | No modifica docker-compose.yml | | --src <dir> | Carpeta base del código (por defecto src) |

Ejecutar dos veces el mismo generador es seguro: los archivos existentes se saltan salvo que uses --force.

Generadores

| Generador | Qué genera | Dependencias | Docker | |---|---|---|---| | clean | Estructura de carpetas Clean Architecture | — | — | | express | Servidor Express 5, tsconfig, scripts dev/build/start, manejo de errores | express, cors, helmet, typescript, tsx, tsup | Dockerfile | | env | Validador de env estilo env-var con adapter Zod | zod | — | | redis | Contrato de cache, servicio, cliente singleton | ioredis | redis:7-alpine | | postgres | Config y Pool singleton | pg | postgres:16-alpine | | prisma | Schema, cliente singleton, scripts | @prisma/client, @prisma/adapter-pg, prisma | postgres:16-alpine | | drizzle | Schema, cliente singleton, scripts | drizzle-orm, pg, drizzle-kit | postgres:16-alpine | | typeorm | DataSource con EntitySchema, singleton, migraciones | typeorm, pg | postgres:16-alpine | | sequelize | Modelos con Model.init, singleton, sequelize:sync | sequelize, pg, pg-hstore | postgres:16-alpine | | mongoose | Conexión singleton, modelo User | mongoose | mongo:7 | | entity <Nombre> | Entidad de dominio | — | — | | repository <Nombre> | Contrato + implementación in-memory | — | — | | repository <Nombre> --orm prisma\|drizzle | Contrato + mapper + repositorio con el ORM | — | — | | usecase <nombre> | Caso de uso | — | — | | user-crud [--orm <orm>] | CRUD de usuarios: entidad, DTOs, contratos, casos de uso, API (+caché si hay redis) | — | — | | datasource user --orm <orm> | Cambia el datasource del CRUD sin tocar el dominio | — | — | | metrics [--path] [--protect] [--no-load-shedding] | Métricas HTTP, de casos de uso y del proceso + dashboard de Grafana + 503 inmediato si se satura | @prometheus-io/client | prom/prometheus, grafana/grafana |

Los ORMs (prisma, drizzle, typeorm, sequelize, mongoose), express y repository --orm requieren la estructura clean; user-crud requiere clean y express; metrics requiere express. Donde aparece <orm> vale prisma, drizzle, typeorm, sequelize, mongoose o memory.

jg g clean

src/
├── domain/api/
│   ├── datasources/  dtos/  repositories/
│   └── entities/  services/  use-cases/
├── infrastructure/
│   ├── api/
│   │   ├── mappers/  datasources/  repositories/
│   │   └── services/        ← implementaciones de servicios (ej. Redis)
│   ├── data/                ← ORMs (Prisma, Drizzle, Postgres)
│   └── redis/               ← config + cliente singleton
└── presentation/

El dominio solo contiene contratos (interfaces) para invertir dependencias; la infraestructura los implementa. Una vez creada esta estructura, el resto de los generadores escribe en ella automáticamente.

jg g express

Deja un proyecto nuevo listo para arrancar:

tsconfig.json          # estricto, module Preserve + moduleResolution Bundler
tsup.config.ts         # build a dist/main.js (ESM)
Dockerfile             # multi-stage, usuario no root
.dockerignore  .gitignore
src/
├── load-env.ts        # carga .env antes que cualquier config
├── main.ts            # arranque + cierre ordenado (SIGINT/SIGTERM)
└── presentation/http/
    ├── app.ts         # helmet, cors, JSON, rutas, 404 y errores
    ├── routes/        # index.ts + health.routes.ts (GET /api/health)
    ├── errors/        # HttpError.badRequest(), .notFound(), ...
    └── middlewares/   # not-found.ts, error-handler.ts

Agrega "type": "module" a package.json y los scripts:

| Script | Comando | |---|---| | npm run dev | tsx watch src/main.ts (recarga al guardar) | | npm run build | tsup | | npm start | node dist/main.js | | npm run typecheck | tsc --noEmit |

Con jg g env previo, el puerto y NODE_ENV se leen validados desde appConfig. Las rutas lanzan HttpError y el manejador central responde con el status y un JSON { error, details }.

jg g clean && jg g express
npm run dev     # http://localhost:3000/api/health

jg g env

import { env } from './infrastructure/env/env.provider';

const port = env.get('PORT').required().asPort();          // number
const debug = env.get('DEBUG').default('false').asBool();  // boolean
const tags = env.get('TAGS').asArray();                    // string[] | undefined
const mode = env.get('NODE_ENV').required().asEnum(['development', 'production']);

Métodos: asString, asInt, asFloat, asPort, asBool, asUrl, asEmail, asEnum, asArray, asJson<T>(). Sin .required() ni .default() el tipo incluye undefined. Un valor inválido lanza EnvValidationError al arrancar.

Si env existe, redis, postgres y los ORMs validan su configuración con él.

jg g redis

import { getCache } from './infrastructure/api/services/cache.provider';

const cache = getCache();                       // CacheService (contrato de dominio)
await cache.set('user:1', { name: 'Ana' }, 60); // TTL en segundos
const user = await cache.get<{ name: string }>('user:1');

jg g prisma / jg g drizzle

Crean la configuración en infrastructure/data/, un modelo/tabla User de ejemplo, el servicio postgres en docker y scripts en package.json:

docker compose up -d postgres

npm run prisma:migrate -- --name init               # Prisma
npm run drizzle:generate && npm run drizzle:migrate  # Drizzle

El cliente de Prisma se genera automáticamente al crear Prisma, el CRUD o al cambiar de datasource. Solo hay que volver a ejecutar npm run prisma:generate si editas schema.prisma a mano.

Luego, un repositorio real:

jg g repository Product --orm prisma
domain/api/entities/product.entity.ts                         # entidad
domain/api/repositories/product.repository.ts                 # contrato
infrastructure/api/mappers/prisma-product.mapper.ts           # toDomain / toPersistence
infrastructure/api/repositories/prisma-product.repository.ts  # implementación

Además agrega model Product a schema.prisma (o la tabla products al schema de Drizzle).

jg g typeorm / jg g sequelize / jg g mongoose

Misma idea que Prisma y Drizzle: configuración en infrastructure/data/<orm>/, cliente singleton, modelo User de ejemplo y base de datos en docker. Ninguno usa decoradores (tsx y tsup compilan con esbuild, que no emite metadata), así que el modelo de persistencia queda en infraestructura:

| ORM | Modelo | Tablas / colecciones | |---|---|---| | TypeORM 1.x | EntitySchema en entities/user.schema.ts | npm run typeorm:generate && npm run typeorm:migrate (migraciones JS ESM) | | Sequelize 6 | Model.init en models/user.model.ts | npm run sequelize:sync (desarrollo; en producción, migraciones) | | Mongoose 9 | Schema en models/user.model.ts | No hacen falta: colección e índice se crean al usarse |

docker compose up -d postgres   # TypeORM / Sequelize
docker compose up -d mongo      # Mongoose (MONGO_URL en .env)

repository --orm sigue soportando solo Prisma y Drizzle; los cinco ORMs funcionan con user-crud y datasource.

jg g user-crud

Genera un CRUD de usuarios (id, name, email único, createdAt, updatedAt) recorriendo todas las capas, y lo registra en /api/users. Conviene ejecutarlo al final, cuando ya están el ORM y Redis:

jg g clean && jg g express && jg g env && jg g redis && jg g prisma
jg g user-crud
domain/api/
├── entities/user.entity.ts                  # UserEntity (readonly + static create)
├── dtos/user/                               # CreateUserDto, UpdateUserDto → [error, dto]
├── datasources/user.datasource.ts           # contrato (abstract class)
├── repositories/user.repository.ts          # contrato (abstract class)
├── errors/user.errors.ts                    # UserNotFoundError, EmailAlreadyInUseError
└── use-cases/user/                          # create, get-users, get-user-by-id, update, delete
infrastructure/api/
├── datasources/prisma-user.datasource.ts    # o drizzle- / typeorm- / sequelize- / mongoose- / in-memory-
├── mappers/prisma-user.mapper.ts            # fila del ORM → UserEntity
└── repositories/user.repository.impl.ts     # delega en el datasource
presentation/http/user/
├── user.composition.ts                      # composition root: arma el grafo
├── user.controller.ts                       # errores de dominio → 404 / 409
└── user.routes.ts
  • Datasource: el ORM configurado; si hay varios, elige con --orm; sin ORM, en memoria.
  • Caché: si existe jg g redis, los casos de uso aplican cache-aside (users:all, users:<id>, TTL 60 s) e invalidan en cada escritura. Sin Redis se generan sin caché.
  • Endpoints: POST /, GET /, GET /:id, PATCH /:id (parcial), DELETE /:id. Validación → 400, no existe → 404, email repetido → 409.

Después de generarlo aplica el modelo (npm run prisma:migrate -- --name users, o drizzle:generate + drizzle:migrate).

jg g datasource user --orm <orm>

Intercambia la base de datos del CRUD en un comando. El dominio solo conoce el contrato UserDatasource, así que el cambio queda en infraestructura:

jg g datasource user --orm drizzle   # prisma → drizzle
jg g datasource user --orm mongoose  # drizzle → MongoDB
jg g datasource user --orm memory    # para tests o demos
jg g datasource user --orm prisma    # vuelta atrás instantánea
  • Crea infrastructure/api/datasources/<orm>-user.datasource.ts y su mapper (y el modelo/tabla si faltan). Si ya existían, los reutiliza.
  • Reescribe solo presentation/http/user/user.composition.ts, el composition root, que es el único archivo que nombra la clase concreta.
  • Entidad, DTOs, contratos, casos de uso, caché, controller y rutas no cambian.
  • Los datasources anteriores se conservan; jg undo restaura el composition root previo.

El ORM destino tiene que estar configurado (jg g <orm>) y con sus tablas creadas. Al pasar entre Postgres y Mongo cambian los ids (UUID ↔ ObjectId): si usas caché, vacíala. Si editaste el composition root a mano, el cambio lo regenera (lo anterior se recupera con jg undo).

jg g metrics

Deja la API monitoreada con Prometheus y Grafana sin escribir código: la app expone /metrics, Prometheus la lee cada 15 s y Grafana lo muestra en un dashboard ya armado. Requiere jg g express.

jg g metrics                              # /metrics abierta
jg g metrics --path /internal/metrics     # otra ruta
jg g metrics --protect                    # exige Authorization: Bearer <token>

npm run dev
docker compose up -d prometheus grafana   # Prometheus :9090 · Grafana :3001 (admin / admin)

Qué se mide, sin instrumentar cada función:

  • Peticiones HTTP (middleware): cantidad, duración y código de respuesta, etiquetadas por el patrón de la ruta (/api/users/:id, nunca la URL real, para no crear una serie por id).
  • Casos de uso (Decorator measured): duración y fallos, sin modificarlos. Si existe el CRUD de usuarios, sus cinco casos de uso se miden automáticamente y jg g datasource lo conserva. Para otro caso de uso: measured(new MiCasoDeUso(...), getMetrics(), 'mi_caso') en su composition root.
  • Proceso: memoria, CPU, event loop.

Cómo queda:

  • Dominio: contrato MetricsService y el Decorator measured. Infraestructura: PrometheusMetricsService (Singleton + Adapter sobre @prometheus-io/client) y su provider. Presentación: middleware, ruta y un router que se conecta en app.ts como primer middleware. Si cambiaste app.ts y no encuentra dónde insertarlo, no lo toca y te dice qué línea añadir.
  • monitoring/: prometheus.yml, datasource y dashboard de Grafana (provisionados, sin configurar nada a mano).
  • .env: METRICS_PATH, puertos y usuario de Grafana; con --protect, METRICS_TOKEN (generado, solo en .env; en .env.example queda vacío).

503 inmediato cuando la app se satura (load shedding), activado por defecto (--no-load-shedding para omitirlo): si el retraso medio del event loop supera LOAD_SHEDDING_MAX_EVENT_LOOP_DELAY_MS (50 ms, la CPU no da abasto), responde 503 + Retry-After al instante a una parte de las peticiones, que sube de a poco hasta el 90% (nunca todas). LOAD_SHEDDING_MAX_IN_FLIGHT limita además las peticiones en curso; viene en 0 (sin límite) porque depende de cada app (capacidad × latencia aceptable): un valor fijo mal elegido rechaza de más cuando el límite es la base de datos. Así las aceptadas no se encolan hasta el timeout. /metrics y /api/health nunca se rechazan, y los rechazos se ven en el dashboard (ruta load_shedding). Ayuda cuanto más cara es cada petición (base de datos, CPU) frente a rechazarla, y cuando los clientes respetan Retry-After; con sobrecargas varias veces mayores que la capacidad hace falta escalar (más instancias o un proxy que limite delante).

Con --protect, como prometheus.yml no lee variables de entorno, la clave también se guarda en monitoring/prometheus/metrics.token (añadido al .gitignore) y se monta en el contenedor.

Acceder con la ruta protegida:

  • Prometheus usa la clave solo; Grafana consulta a Prometheus, así que tampoco la necesita.
  • A mano: curl -H "Authorization: Bearer $(cat monitoring/prometheus/metrics.token)" localhost:3000/metrics. El navegador no envía cabeceras Bearer: para mirar, usa Grafana o Prometheus.
  • Al clonar el repo (la clave no se commitea) o para rotarla: npm run metrics:token genera una nueva y la escribe en .env y en metrics.token. Sin clave, la app no arranca ("Falta METRICS_TOKEN") en vez de quedar abierta.
  • En producción: define METRICS_TOKEN en el entorno del servidor y configura tu Prometheus con la misma clave. Prometheus lee la app en host.docker.internal:<PORT>: si cambias PORT, cámbialo también en prometheus.yml.

Deshacer cambios

Cada generación queda registrada en .jg/history.json (con su propio .gitignore, nunca se commitea).

jg history            # 1 = la más reciente
jg undo               # deshace la última
jg undo 3             # deshace las últimas 3
jg undo --dry-run     # muestra qué revertiría
jg undo --force       # revierte aunque hayas editado los archivos

El undo borra los archivos creados, restaura los modificados (docker-compose.yml, .env, schemas), elimina carpetas vacías, quita los scripts agregados y desinstala los paquetes que instaló. Si editaste un archivo después de generarlo, se detiene para no perder tu trabajo.

Configuración

jg init crea jg.config.json:

{
  "srcDir": "src",
  "importExtension": "",
  "packageManager": null
}

| Campo | Descripción | |---|---| | srcDir | Carpeta base del código | | importExtension | "" para CommonJS/bundlers (Next.js, Vite); ".js" para ESM con "module": "NodeNext" | | packageManager | Fuerza npm, pnpm, yarn o bun (por defecto se detecta) |

Agregar un generador

  1. Crea las plantillas en templates/<nombre>/*.tpl. Variables disponibles: {{pascal}}, {{camel}}, {{kebab}}, {{snake}}, {{pluralCamel}}, {{pluralSnake}}, {{srcDir}}, {{project}}. Para variantes usa bloques {{#if flag}}…{{else}}…{{/if}} con un vars: { flag: true }.
  2. Crea src/generators/<nombre>.js con un plan() que declare files, dependencies, env, compose, scripts, etc. Los imports entre archivos se calculan solos con imports.
  3. Regístralo en src/generators/index.js.

Escribir archivos, editar el compose y el .env, instalar paquetes y el undo son comunes a todos los generadores. Usa src/generators/redis.js como referencia.

Desarrollo

git clone https://github.com/JavierGmnDEV/jagg-cli.git
cd jagg-cli
npm install
npm link        # deja `jg` disponible globalmente apuntando a este código

Licencia

MIT