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

@sundaysf/cli-v3

v0.0.2

Published

Sundays Framework v3 CLI - scaffolds an Express 5 + Knex + Postgres API and generates entity verticals

Readme

Sundays Framework v3 — @sundaysf/cli-v3

CLI that scaffolds a production-ready REST API (Express 5 · TypeScript · Knex · PostgreSQL · Zod · pino · Jest) and generates complete entity verticals inside it.

Commands

# Create a project ---------------------------------------------------------------
npx @sundaysf/cli-v3 new my-api                 # new folder ./my-api
npx @sundaysf/cli-v3 new my-api --with-auth     # + user/auth tables, register/login/me, JWT
npx @sundaysf/cli-v3 new .                      # scaffold INTO the current (empty) folder
npx @sundaysf/cli-v3 new . --name my-api        # same, choosing the package name
npx @sundaysf/cli-v3 new my-api -y              # no prompts, defaults (no auth, port 3005, npm)

# Options for `new`
#   --with-auth | --no-auth     include / skip auth without asking
#   --port <n>                  HTTP port (default 3005)
#   --pm npm|pnpm               package manager (auto-detected)
#   --no-install                skip dependency installation
#   --no-git                    skip git init and the initial commit
#   --name <name>               project name when using "."
#   -y, --yes                   accept every default, never prompt

# Work inside the project --------------------------------------------------------
docker compose up -d                            # local PostgreSQL 16
npm run db:migrate                              # apply migrations
npm run start:dev                               # http://localhost:3005/api/health
npm test                                        # jest + coverage (needs Postgres)
npm run test:unit                               # jest without database suites

# Generate an entity (run inside the project) ------------------------------------
sundaysf generate entity product name:string:unique price:decimal isActive:boolean=true
sundaysf g entity order userId:user.id total:decimal 'notes:text?' status:string=pending
sundaysf g entity tag --dry-run                 # print, write nothing (asks for fields on a TTY)

# Options for `generate entity`
#   --fields "<spec>"           fields as one string instead of positional args
#   --no-tests                  skip unit + route tests
#   --no-migration              skip the migration
#   --dry-run                   print everything, write nothing
#   --force                     overwrite existing code files (never migrations)

# After generating
npm run db:migrate && npm run typecheck && npm test

# Global install instead of npx ----------------------------------------------------
npm install -g @sundaysf/cli-v3                 # then: sundaysf new ..., sundaysf g entity ...
sundaysf --help  |  sundaysf new --help  |  sundaysf generate entity --help

Field syntax: name:type[?][:unique][=default] with string text integer decimal boolean date datetime json uuid <entity>.id. Quote fields containing ? in zsh ('notes:text?'). Full reference: sundaysf generate entity.

What changed since v2

| | v2 (@sundaysf/cli-v2) | v3 (@sundaysf/cli-v3) | | ----------- | ------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- | | Templates | 6 (--backend, --db-sql, --backend-embedded-db-sql...) | 1: API with Knex embedded, plus an optional auth overlay | | Commands | --init --<template>, --create-controller | new, generate entity | | Generators | controller + router stubs; dead manifest.js; scripts/import-manifest.js copied into projects | one generator: migration, interface, DAO, DTOs, controller, router, unit + route tests, barrel export | | Express | 4 | 5 | | Validation | hand-written DTO classes, phantom @sundaysf/utils | Zod schemas in the same dto/input/<e>/ layout | | Data access | every DAO re-implements 6 CRUD methods | BaseDAO<T> with pagination and optional transaction | | Config | duplicated in knexfile.ts and KnexConnection.ts, process.env everywhere | one knex.config.ts; env validated once with Zod | | Dev tooling | nodemon + ts-node | tsx for the dev server and the knex CLI | | Logging | morgan + console.* | pino + pino-http with request ids | | Security | none | helmet, CORS allowlist, graceful shutdown | | Tests | none shipped | Jest + supertest suite included, ~97% coverage, CI workflow | | Local DB | bring your own | docker-compose.yml with PostgreSQL 16 | | Docs | stale README/CLAUDE.md | README, CLAUDE.md and Claude agents rewritten for the real code | | Removed | ownLibs/, postman.json, .npmrc in projects, root migrations/, lodash, rimraf, expo lint | |

Install

npm install -g @sundaysf/cli-v3     # global: `sundaysf ...`
npx @sundaysf/cli-v3 new my-api      # or one-off

Requires Node.js 20+ (the generated project targets Node 22+), npm or pnpm, git, and Docker for the local database (optional).

sundaysf new

sundaysf new <name|.> [options]

  .                  scaffold into the current directory (must be empty; an existing .git is kept)
  --name <name>      project/package name when using "." (default: the folder name, slugified)
  --with-auth        include auth (user/auth tables, register/login/me, JWT, bcrypt, middleware, tests)
  --no-auth          skip auth without asking
  --port <port>      HTTP port (default 3005)
  --pm <npm|pnpm>    package manager (auto-detected)
  --no-install       skip dependency installation
  --no-git           skip git init and the initial commit
  -y, --yes          accept defaults, never prompt

Interactive runs ask for anything not given as a flag. With --yes or without a TTY the defaults are: no auth, port 3005, install, git.

What it does:

  1. Validates the name (^[a-z0-9][a-z0-9-]{0,63}$) and that the target folder is empty. With . the folder is the current directory: only .git and editor folders may already exist, and the name comes from --name or the folder name (My API v2 → my-api-v2).
  2. Copies the api template, applies the api-auth overlay when requested, replaces the __SF_*__ tokens (project name, port, database name, CLI version), renames _gitignore and _package.json, and refuses to finish if any token is left or a .npmrc sneaked in.
  3. Writes .env from .env.example. With auth, .env gets a random 64-char JWT_SECRET (.env.example keeps a placeholder).
  4. git init -b main (skipped when .git already exists), npm install, npm run format, and an initial commit (chore: scaffold <name> with @sundaysf/cli-v3 <version>).
  5. Prints the next steps. A failing install or commit is reported but never deletes the files.

Generated tree (auth files marked *):

my-api/
├── .env  .env.example  .sundaysrc  .gitignore  .dockerignore  .prettierrc  eslint.config.js
├── package.json  tsconfig.json  tsconfig.spec.json  jest.config.js  jest.setup.js  knexfile.ts
├── docker-compose.yml  Dockerfile  README.md  CLAUDE.md
├── .claude/agents/{sundays-backend-builder,knex-table-implementer}.md
├── .github/workflows/{ci,deploy}.yaml
└── src/
    ├── server.ts                      boot, migrations on demand, graceful shutdown
    ├── app.ts                         helmet, cors, request id, pino-http, parsers, /api, 404, errors
    ├── common/{config/env,logger,errors/http.error,validation/parse-dto,utils/*}.ts
    ├── db/{index,BaseDAO,KnexConnection,knex.config,d.types}.ts
    ├── db/dao/<entity>/<entity>.dao.ts            db/interfaces/<entity>/<entity>.interfaces.ts
    ├── migrations/  seeds/
    ├── routes/index.ts (auto-discovery)  routes/health/  routes/auth/*
    ├── controllers/health/  controllers/auth/*
    ├── dto/input/auth/*  services/jwt/*  services/password/*  middlewares/auth/*
    ├── middlewares/{error,not-found,request-id}/
    └── jobs/  (cron jobs: export run() + schedule(), register schedule() in server.ts)

sundaysf generate entity

sundaysf generate entity <name> [fields...] [options]      (alias: sundaysf g entity)

  --fields "<spec>"   fields as one string instead of positional arguments
  --no-tests          skip the unit and route tests
  --no-migration      skip the migration
  --dry-run           print everything, write nothing
  --force             overwrite existing code files (migrations are never overwritten)

Must run inside a project created by v3 (it looks for .sundaysrc with a cli field and the @sundays markers in src/db/index.ts). Without fields on a TTY it asks for them one by one.

Field syntax

name:type[?][:unique][=default]

| Type | Column (knex) | TypeScript | Zod (create) | | ------------- | -------------------------------------------------------------------------------- | ------------------------- | ----------------------------------- | | string | string(name, 255) | string | z.string().trim().min(1).max(255) | | text | text(name) | string | z.string() | | integer | integer(name) | number | z.number().int() | | decimal | decimal(name, 15, 2) | number | z.number() | | boolean | boolean(name) | boolean | z.boolean() | | date | date(name) | string (YYYY-MM-DD) | z.iso.date() | | datetime | timestamp(name) | Date \| string | z.coerce.date() | | json | jsonb(name) | Record<string, unknown> | z.record(z.string(), z.unknown()) | | uuid | uuid(name) | string | z.uuid() | | <entity>.id | integer(name).references('id').inTable('<entity>').onDelete('CASCADE').index() | number | z.number().int().positive() |

| Modifier | Column | TypeScript | Zod | | --------- | ------------------------------------------------ | ------------------- | ------------------------ | | (none) | .notNullable() | required | required | | ? | nullable | field?: T \| null | .nullable().optional() | | :unique | .unique() + getBy<Field>() finder in the DAO | | | | =value | .defaultTo(value) | field?: T | .default(value) |

id, uuid, createdAt and updatedAt are always added and cannot be declared. Field names are camelCase; the entity name can be written in any case (productCategory, product-category, ProductCategory) and is derived into kebab (files, /api/product-category), Pascal (ProductCategoryDAO, IProductCategory), camel (_productCategoryDAO) and snake (product_category table).

Files written

src/migrations/<timestamp>_create_<table>.ts
src/db/interfaces/<kebab>/<kebab>.interfaces.ts        interface I<Pascal> extends IEntity
src/db/dao/<kebab>/<kebab>.dao.ts                      class <Pascal>DAO extends BaseDAO<I<Pascal>>
src/dto/input/<kebab>/<kebab>.create.dto.ts            <Pascal>CreateSchema + validate<Pascal>Create()
src/dto/input/<kebab>/<kebab>.update.dto.ts            <Pascal>UpdateSchema (all optional, no defaults)
src/controllers/<kebab>/<kebab>.controller.ts          getAll, getByUuid, create, update, delete
src/routes/<kebab>/<kebab>.router.ts                   GET /, GET /:uuid, POST /, PUT /:uuid, DELETE /:uuid
src/controllers/<kebab>/__tests__/<kebab>.controller.test.ts   unit test, DAO mocked
src/routes/<kebab>/__tests__/<kebab>.routes.test.ts            supertest lifecycle against Postgres
src/db/index.ts                                        two export lines added above the markers

The route test is written as describe.skip when the entity has foreign keys: the header comment explains which parent rows to create in beforeAll before enabling it. Generated files are formatted with the project's prettier.

Markers

Generators never parse TypeScript; they insert lines above marker comments, skipping lines that already exist. The base project ships these markers, keep them:

| File | Marker | | --------------------------- | -------------------------------------------------------------------- | | src/db/index.ts | // @sundays:interfaces, // @sundays:daos | | src/common/config/env.ts | // @sundays:env-schema | | .env.example | # @sundays:env | | .github/workflows/ci.yaml | # @sundays:ci-env | | README.md | <!-- @sundays:readme-env -->, <!-- @sundays:readme-endpoints --> | | CLAUDE.md | <!-- @sundays:features --> |

The generated API

Request flow: routes/<x>/<x>.router.ts (auto-mounted at /api/<x>) → controllers/<x> → dto/input/<x> for validation → db/dao/<x> (BaseDAO) → PostgreSQL. Cross-cutting logic goes in services/<x>, scheduled work in jobs/.

  • src/server.ts validates the environment, connects KnexManager, runs migrations when RUN_MIGRATIONS=true, imports app.ts and listens. SIGTERM/SIGINT close the HTTP server and the pool (10 s timeout).
  • src/app.ts wires helmet, cors (from CORS_ORIGINS), the request id middleware, pino-http (skips /api/health), body parsers, /api, the 404 handler and the error handler. A commented hook shows where to mount raw-body webhooks (Stripe) before the JSON parser.
  • src/common/config/env.ts is a Zod schema; env is typed and a bad deploy fails at boot.
  • src/common/errors/http.error.ts exports HttpError and badRequest(), unauthorized(), forbidden(), notFound(), conflict(). The error middleware renders them as { success: false, message, errors? } and hides 500 messages in production.
  • src/common/validation/parse-dto.ts turns a Zod failure into HttpError(400) with { field: [messages] }.
  • src/db/BaseDAO.ts gives every DAO create, getById, getByUuid, update, delete and getAll(page, limit) (returns IDataPaginator), each with an optional trx. Subclasses declare protected readonly table and add finders with this.q(trx).
  • src/db/knex.config.ts is the only knex configuration; knexfile.ts re-exports it for the CLI. Migrations and seeds live in src/ and compile with the app (loadExtensions follows the running extension: .ts under tsx/jest, .js from dist/).
  • Auth overlay adds user + auth tables, UserDAO/AuthDAO, JwtService, PasswordService, authMiddleware/optionalAuthMiddleware (sets req.auth), the /api/auth/register|login|me endpoints, and their tests.

Conventions

  • Envelope: { success: true, data } / { success: false, message, errors? }; lists return { success, data, page, limit, count, totalCount, totalPages }.
  • Public identifier uuid, internal numeric id. Routes take /:uuid.
  • Tables snake_case, columns camelCase, every table has id, uuid, createdAt, updatedAt.
  • Classes: XRouter (public router: Router, handlers bound with .bind()), XController (private _xDAO = new XDAO(), try { } catch (err) { next(err) }), XDAO extends BaseDAO<IX>.
  • Express 5: no bare * (use /*splat), optional params {/:id}, req.query is read-only, req.body is undefined without a matching parser.
  • Logging through req.log / logger, never console.

Testing the generated API

| Command | Needs Postgres | What runs | | ------------------- | -------------- | ---------------------------------------------------------------------- | | npm test | yes | everything, with coverage (jest.config.js threshold, 80% by default) | | npm run test:unit | no | everything except src/routes/** and src/db/** | | npm run typecheck | no | tsc --noEmit on sources and tests |

docker compose up -d starts PostgreSQL 16 with the credentials in .env.example. jest.setup.js aborts if SQL_HOST is not local unless ALLOW_REMOTE_DB_TESTS=1. Controller tests mock the src/db barrel; route tests use supertest against the real app and clean up after themselves.

Deploying the generated API

  • Dockerfile: two-stage node:24-alpine build, npm ci --omit=dev runtime, node dist/server.js.
  • .github/workflows/ci.yaml: typecheck, lint, migrate, test and build on every PR/push with a Postgres service.
  • .github/workflows/deploy.yaml: manual (workflow_dispatch) build → ECR push → ECS task definition render → service deploy. Needs secrets.AWS_ROLE_ARN (OIDC) and vars.AWS_REGION; ECR/ECS resource names default to the project slug.
  • .sundaysrc carries runtime, install, build, start, port and the generating cli version for the Sundays platform.

Developing the CLI

src/
  cli.ts                     commander program
  commands/{new,generate-entity}.ts
  core/                      naming, fields DSL, template engine, overlay, inject, project lookup, exec, prompts
  generators/entity/         context + render/* (one renderer per file) + index (writes + barrel)
templates/api/               the base project (tokens: __SF_PROJECT_NAME__, __SF_PROJECT_SLUG__,
                             __SF_DB_NAME__, __SF_PORT__, __SF_CLI_VERSION__, __SF_YEAR__, __SF_JWT_SECRET__)
templates/api-auth/          overlay: new files + overlay.json (package.json merge + marker injections)
test/unit                    DSL, naming, engine, overlay, inject
test/snapshot                every rendered entity file (update with `npx vitest run -u`)
test/e2e                     scaffold + typecheck + generate (+ migrate + jest with SUNDAYS_E2E_DB=1)
npm install
npm run dev -- new demo --yes         # run from source (tsx)
npm run build && npm link             # real `sundaysf` binary from dist/cli.js
npm test                              # unit + snapshot (fast)
SUNDAYS_E2E=1 npm run test:e2e        # scaffolds into a temp dir, needs network for npm install
SUNDAYS_E2E=1 SUNDAYS_E2E_DB=1 SQL_HOST=localhost SQL_PORT=5432 SQL_USER=postgres \
  SQL_PASSWORD=postgres SQL_DB_NAME=e2e_api npm run test:e2e   # + migrate + jest
npm pack --dry-run                    # check templates/ and _gitignore ship, .npmrc does not

To change the base project edit templates/api directly (it is a real project: copy it somewhere, npm install, run it). Files npm would strip from a tarball are named _gitignore, _dockerignore, _package.json and renamed on copy (RENAME_MAP in core/template-engine.ts). To add a field type, extend FIELD_TYPES and the four mappings in core/fields.ts, then update the snapshot. To add an overlay, create templates/<name>/ with an overlay.json.

Publishing: npm publish runs prepublishOnly (build + tests). The registry token belongs in the developer's ~/.npmrc, never in the repo or the templates.

Troubleshooting

  • Marker "// @sundays:daos" not found — the barrel lost its markers; add the two comment lines back to src/db/index.ts (see Markers).
  • ts-jest errors about TypeScript version — the project pins typescript ~5.9; TypeScript 7 is not supported by ts-jest yet. Do not bump it.
  • SQL_HOST="..." is not local — the test guard refused a remote database. Point .env at localhost or export ALLOW_REMOTE_DB_TESTS=1 on purpose.
  • PathError / routes not matching after copying Express 4 code — Express 5 changed the path syntax: * → /*splat, /:id? → {/:id}, no regex in strings.
  • Invalid environment configuration — a required variable is missing from .env; the message lists the fields. Add it to .env (and to the schema if it is new).
  • npm run start:dev runs code with type errors — tsx does not type-check; run npm run typecheck (CI does).
  • Port 5432 already in use — another Postgres is running; change the host port in docker-compose.yml and SQL_PORT in .env.

License

MIT