expcli
A developer CLI for scaffolding and managing Express + TypeScript projects — published on npm as expcli-ts. Generates projects from opinionated templates, scaffolds feature modules, and adds integrations (databases, auth, testing, logging, docs, security) with a single command.
Full Tech Stack
Overview
expcli is a CLI tool that brings a structured, opinionated development workflow to Express + TypeScript projects. It handles the full lifecycle: scaffolding new projects from templates, generating individual files or full feature modules, and wiring in third-party integrations without manual configuration. The CLI is ESM-only, targets Node 18+, auto-detects the active package manager, works in both interactive TTY and non-interactive CI environments, and ships with a custom token-based template engine — no EJS or Handlebars dependency. Published on npm as expcli-ts with a GitHub Actions pipeline that builds, tests across Node 18/20/22, and publishes on every version tag.
Repositories
expcli
Node.js · TypeScript · Commander.jsSingle-repository CLI tool published as expcli-ts on npm. The binary is expcli. All commands — new, generate, add, remove, build, start, start-dev, info, list, update — are registered through Commander.js and backed by a layered internal architecture: a custom template engine, a generator system, an integration registry, a package manager abstraction, and an anchor-based file patcher.
- Custom __TOKEN__ template engine with conditional block support (//__IF_FLAGNAME__ ... //__END_IF_FLAGNAME__) — no EJS or Handlebars dependency. Validates that no unreplaced tokens remain after rendering.
- Three project templates (minimal, rest-api, full) rendered from .tpl files at scaffold time. Templates are bundled into dist/templates/ by tsup via a post-build copy step.
- 11 generator schematics: module, controller, service, route, model, dto, middleware, guard, exception-filter, pipe, spec — each with a short alias. The module schematic scaffolds a fully wired controller/service/routes/types/dto set in one command.
- 19 integrations across 8 categories (validation, docs, testing, security, logging, databases, auth, infrastructure). Each integration installs packages, scaffolds config files, and patches src/app.ts at named anchor comments — fully reversible via expcli remove.
- Anchor-based file patcher inserts and removes code blocks at // @expcli: comment anchors. All patch operations are idempotent — safe to run multiple times without duplicating injected lines.
- Auto-detects package manager from user-agent env, lockfile presence, and PATH availability — falls back to npm. Supports npm, yarn, pnpm, and bun throughout all install and add/remove flows.
- Non-TTY safe throughout: all @clack/prompts usage is guarded with process.stdout.isTTY checks and falls back to plain console output — works correctly in CI and piped environments.
- expcli.json config file tracks project metadata, registered modules, and installed integrations. Validated with zod on every read. Used by generate, add, remove, info, and build commands to operate on the correct project context.
- expcli generate spec creates a supertest spec file for an existing module. Auto-detects vitest vs jest from expcli.json integrations and conditionally emits the correct import block via the template engine flag system.
- GitHub Actions CI matrix tests across Node 18, 20, and 22 on every push to main. A separate publish workflow triggers on v* tags, builds, tests, and runs npm publish --access public using a scoped NPM_TOKEN secret.
Architecture
Template engine processes conditional blocks first, then replaces tokens, then validates no tokens remain — single-pass, no AST, no external parser.
Generator system uses a factory function with dynamic imports per schematic type. Each generator extends BaseGenerator which handles dry-run mode, file existence prompts, and rendering — concrete generators only define which templates map to which output paths.
Integration registry holds 19 statically typed integration objects. Each implements IIntegration: packages, requires[], conflictsWith[], run() and remove() methods. The add command resolves the chain, checks conflicts, prompts confirmation, and delegates to the integration.
Package manager abstraction detects the active PM once and returns a unified commands object (install, add, remove) — all consumers call the same interface regardless of which PM is in use.
Anchor-based patcher operates on plain text files using named comment anchors (// @expcli:imports, // @expcli:middleware, etc.). Inserts a code block on the line after the anchor; remove() scans for the block and deletes it. No AST parsing required.
tsup bundles src/main.ts to dist/main.js (ESM). A post-build script copies templates/ into dist/templates/ so the published package is self-contained. The bin entry at bin/expcli.js sets the shebang and requires dist/main.js.
expcli.json is the project manifest — version, name, template, packageManager, srcRoot, modulesDir, entryFile, outDir, integrations[], modules[]. findConfigUp() walks parent directories to locate it, so commands work from any subdirectory of the project.