@sajaddp/typescript-template
v2.1.1
Published
A polished TypeScript CLI starter for Node.js 24+, pnpm 11+, Commander.js, typed env validation, and Vitest.
Maintainers
Readme
TypeScript CLI Starter
A polished TypeScript CLI starter for building Node.js 24+ command-line tools with pnpm 11+, Commander.js, typed environment validation, Vitest testing, Biome formatting, and a clean developer experience.
Use this repo when you want a practical Node.js CLI template that is still easy to understand. It includes a working ts-template command, production build output, JSON-friendly command responses, safe environment handling, and tests that show how to keep CLI behavior reliable.
Features
- TypeScript CLI starter built for Node.js 24+
- Public
ts-templatebinary with real commands - Commander.js command routing with helpful
--helpoutput - Typed env validation with
zodanddotenv - Safe public config output that never prints unrelated secrets
- Rich terminal output with
@clack/promptsandpicocolors - Fast local development with
tsx - Vitest unit tests for command behavior and error paths
- Biome for formatting, linting, and import organization
- Watch-mode scripts for tests and typechecking
- One-command local verification with npm package content checks
- Clean
distbuild with declarations and source maps
Requirements
- Node.js 24 or newer
- pnpm 11 or newer
Check your local versions:
node --version
pnpm --versionQuick Start
Clone the template, install dependencies, and run the CLI in development mode:
git clone https://github.com/sajaddp/typescript-template.git
cd typescript-template
pnpm install
cp .env.example .env
pnpm dev -- --helpRun the sample commands:
pnpm dev -- hello Sajad
pnpm dev -- env
pnpm dev -- env --json
pnpm dev -- doctor
pnpm dev -- doctor --jsonBuild and run the compiled CLI:
pnpm build
node dist/cli.js --help
node dist/cli.js hello AdaCLI Usage
The package exposes a ts-template binary after build or package installation.
ts-template --help
ts-template hello [name]
ts-template env [--json]
ts-template doctor [--json]hello [name]
Prints a friendly greeting and demonstrates the human-readable CLI style.
pnpm dev -- hello Sajad
pnpm dev -- hello Sajad --jsonenv
Validates public environment configuration and prints only safe values.
pnpm dev -- env
pnpm dev -- env --jsonThe command reads only:
APP_NAMELOG_LEVEL
It does not print unrelated variables such as tokens, passwords, or secrets.
doctor
Checks whether the local environment matches the intended Node.js CLI template workflow.
pnpm dev -- doctor
pnpm dev -- doctor --jsonThe doctor report checks:
- Node.js version is 24 or newer
- pnpm 11+ is being used when command metadata is available
- public environment values pass validation
Environment Configuration
Create a local .env file from the example:
cp .env.example .envDefault values:
APP_NAME="ts-template"
LOG_LEVEL="info"Allowed LOG_LEVEL values:
debuginfowarnerror
Environment validation lives in src/config/env.ts. The schema is intentionally small so it is easy to extend when you add real app settings.
Project Structure
.
├── src/
│ ├── cli.ts # Executable CLI entrypoint
│ ├── index.ts # Public exports and Commander.js program setup
│ ├── commands/ # Individual CLI command handlers
│ ├── config/ # Typed environment validation
│ └── lib/ # Shared CLI context and output helpers
├── .github/
│ └── workflows/ # CI and npm Trusted Publishing workflows
├── docs/
│ ├── development.md # Local development workflow and quality gates
│ └── release.md # npm Trusted Publishing release guide
├── tests/
│ └── cli.test.ts # Vitest coverage for env, routing, JSON, failures
├── scripts/
│ ├── check-package.mjs
│ │ # Validates npm dry-run package contents
│ ├── clean.mjs
│ │ # Removes generated local artifacts
│ └── mark-bin-executable.mjs
│ # Marks the compiled bin as executable after build
├── .editorconfig # Shared editor formatting defaults
├── .env.example # Safe public env example
├── .node-version # Node.js major version for local tools and CI
├── biome.json # Formatter and linter config
├── CONTRIBUTING.md # Contributor workflow and repository rules
├── package.json # Scripts, dependencies, bin, and package metadata
├── pnpm-lock.yaml # Locked dependency graph
├── pnpm-workspace.yaml # pnpm build-script policy for trusted tooling
└── tsconfig.json # Focused Node.js 24 CLI TypeScript configDevelopment Workflow
Use these commands during day-to-day development:
pnpm dev:help
pnpm dev:hello
pnpm test:watch
pnpm typecheck:watch
pnpm fix
pnpm checkRun the full local quality gate before a pull request or release:
pnpm verifypnpm verify runs formatting checks, linting, typechecking, tests, a fresh build, compiled CLI smoke tests, and npm package content checks.
See docs/development.md for the full development workflow.
Testing
This starter uses Vitest for fast TypeScript tests. The current test suite covers:
- default environment loading
- invalid environment values
env --jsonoutput- failure exit codes
hello --jsondoctor --json- old Node.js version detection
Run the suite:
pnpm testBuild
Compile TypeScript into dist:
pnpm buildThe build emits:
- JavaScript files
- Type declaration files
- source maps
Run the compiled CLI:
node dist/cli.js --helpLocal Binary Testing
After building, link the package locally if you want to test the real ts-template command:
pnpm build
pnpm link --global
ts-template --help
ts-template doctorUnlink when finished:
pnpm unlink --global @sajaddp/typescript-templateCustomization Guide
Rename the CLI
Update these places:
package.jsonbinsrc/index.tsCLI_NAME- README command examples
Then rebuild:
pnpm buildAdd a New Command
- Create a new file in
src/commands. - Export a command runner that accepts options and
CliContext. - Register it in
src/index.ts. - Add Vitest coverage in
tests/cli.test.ts. - Document the command in this README.
Keep command handlers easy to test by writing to the injected stdout and stderr streams instead of using console.log directly.
Practical Example: Add status
This example adds a status command with both human-readable and JSON output.
Create src/commands/status.ts:
import type { CliContext } from "../lib/context.js";
import { writeLine } from "../lib/context.js";
import { renderJson } from "../lib/output.js";
type StatusOptions = {
json?: boolean;
};
type StatusPayload = {
ok: true;
service: string;
status: "ready";
};
export const createStatusPayload = (): StatusPayload => ({
ok: true,
service: "ts-template",
status: "ready",
});
export const runStatusCommand = (
options: StatusOptions,
context: CliContext,
): number => {
const payload = createStatusPayload();
if (options.json) {
renderJson(context, payload);
return 0;
}
writeLine(context.stdout, `${payload.service}: ${payload.status}`);
return 0;
};Register it in src/index.ts:
import { runStatusCommand } from "./commands/status.js";Export it with the other command helpers:
export { runStatusCommand } from "./commands/status.js";Add the command inside createProgram, next to the existing commands:
program
.command("status")
.description("Show whether the CLI is ready.")
.option("--json", "Print machine-readable JSON output.")
.action((options: JsonOption) => {
throwOnFailure(
runStatusCommand(
{
json: Boolean(options.json),
},
context,
),
);
});Add a Vitest case in tests/cli.test.ts:
it("reports status as JSON", async () => {
const io = createTestIo();
const exitCode = await runCli(["node", "ts-template", "status", "--json"], {
...io,
env: {},
});
expect(exitCode).toBe(0);
expect(JSON.parse(io.stdout.toString())).toEqual({
ok: true,
service: "ts-template",
status: "ready",
});
});Run it during development:
pnpm dev -- status
pnpm dev -- status --json
pnpm testBuild and smoke test the compiled CLI:
pnpm build
node dist/cli.js status
node dist/cli.js status --jsonAdd More Env Values
- Add the variable to
.env.example. - Extend
appEnvSchemainsrc/config/env.ts. - Update tests for defaults, valid values, and invalid values.
- Decide whether the value is safe to show in the
envcommand.
Do not print secrets in human-readable output or JSON output unless that is the explicit purpose of your CLI.
Release Checklist
Target release version: 2.1.0.
Before publishing or tagging a release:
pnpm verifyUse these package-specific checks when changing package metadata or build output:
pnpm pack:check
pnpm pack:dryPublishing
The selected npm package name is @sajaddp/typescript-template. The package keeps the installed CLI binary name as ts-template.
Automated publishing is prepared through GitHub Releases and npm Trusted Publishing with GitHub Actions OIDC. No NPM_TOKEN is required, and no long-lived npm token should be added to GitHub Secrets.
Publishing is handled by .github/workflows/publish.yml after the npm Trusted Publisher is configured on npmjs.com. See docs/release.md for the full release steps for version 2.1.0.
Troubleshooting
process is not typed
Make sure @types/node is installed and types: ["node"] exists in tsconfig.json.
ts-template is not found
Build first, then use the compiled file directly:
pnpm build
node dist/cli.js --helpFor a real binary command, link the package globally with pnpm link --global.
Env validation fails
Check .env and use supported values:
APP_NAME="ts-template"
LOG_LEVEL="info"Then run:
pnpm dev -- envJSON output is needed for automation
Use --json on commands designed for scripting:
pnpm dev -- env --json
pnpm dev -- doctor --jsonLicense
Released under the MIT License.
