Thanks for considering contributing to devtools-cli! 🎉
git clone https://github.com/yvng-jie/devtools-cli.git
cd devtools-cli
pnpm install
pnpm build # Build dist/ (required before running dt)
npm link # Make dt available globally (optional, for demo)pnpm dev <command> # Run in dev mode (e.g. pnpm dev uuid)
pnpm build # Build for production → dist/
pnpm test # Run tests (vitest)
pnpm lint # Check code style (ESLint)
pnpm typecheck # TypeScript type checksrc/
index.ts — CLI entry point (router)
interactive.ts — Interactive mode
help.ts — Help & version output
utils.ts — Shared utilities (readStdinSync, copyToClipboard)
errors.ts — ExitError class & exit helpers
data/ — Static data files
named-colors.ts — 148 CSS named colors
commands/
types.ts — Command interface
index.ts — Command registry (aggregates all commands)
uuid.ts — UUID generation
base64.ts — Base64 encode/decode
color.ts — Color conversion
jwt.ts — JWT decode
hash.ts — SHA hashing
timestamp.ts — Unix timestamp conversion
__tests__/ — Unit tests
Every command is defined by a Command object:
// src/commands/types.ts
export interface Command {
name: string
aliases: string[] // e.g. ['ts'] for timestamp
description: string // Shown in `dt help`
run: (args: string[]) => void
help: () => void
}Commands are registered in src/commands/index.ts — when you add a new command file, import it there:
// src/commands/index.ts
import { yourCommand } from './your-command.js'
export const commands: Command[] = [
// ... existing commands,
yourCommand,
]The router (src/index.ts) and interactive mode (src/interactive.ts) automatically pick up new commands from the registry — no manual switch-case or menu edits needed.
This step-by-step guide walks you through adding a hypothetical echo command.
Create src/commands/echo.ts:
import chalk from 'chalk'
import { exitWithError } from '../errors.js'
import { readStdinSync } from '../utils.js'
import type { Command } from './types.js'
export function echo(args: string[]) {
const jsonMode = args.includes('--json')
const filteredArgs = args.filter((a) => a !== '--json')
const input = filteredArgs.join(' ') || readStdinSync()
if (!input) {
exitWithError('no input provided')
}
if (jsonMode) {
console.log(JSON.stringify({ input }))
return
}
console.log(input)
}
function echoHelp() {
console.log(chalk.bold('\n echo — Echo back the input'))
console.log(` ${chalk.dim('────')}`)
console.log('')
console.log(` ${chalk.yellow('Usage:')}`)
console.log(' dt echo <text>')
console.log(' echo <text> | dt echo')
console.log('')
console.log(` ${chalk.yellow('Examples:')}`)
console.log(' dt echo "hello world"')
console.log(' echo "hello" | dt echo')
console.log('')
}
export const echoCommand: Command = {
name: 'echo',
aliases: [],
description: 'Echo back the input text',
run: echo,
help: echoHelp,
}Key rules:
- Export both the run function (
echo) and the command object (echoCommand) - The run function is exported separately so tests can call it directly
- Use
exitWithError()for errors, neverprocess.exit() - Always use
.jsextension for imports (ESM) - Use
chalkfor colored output, never raw ANSI codes
Open src/commands/index.ts and add your command:
import { echoCommand } from './echo.js'
export const commands: Command[] = [
uuidCommand,
base64Command,
colorCommand,
jwtCommand,
hashCommand,
timestampCommand,
echoCommand, // <-- add here
]That's it! The command will now appear in:
dt help— listed automaticallydt echo— runs the commanddt echo --help— shows help- Interactive mode — appears in the menu
dt help echo— shows help
Create src/commands/__tests__/echo.test.ts:
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { echo } from '../echo.js'
import { ExitError } from '../../errors.js'
beforeEach(() => {
vi.restoreAllMocks()
})
describe('echo', () => {
it('should echo input', () => {
const spy = vi.spyOn(console, 'log').mockImplementation(() => {})
echo(['hello world'])
expect(spy).toHaveBeenCalledWith('hello world')
})
it('should support --json output', () => {
const spy = vi.spyOn(console, 'log').mockImplementation(() => {})
echo(['hello', '--json'])
expect(spy).toHaveBeenCalledWith(JSON.stringify({ input: 'hello' }))
})
})pnpm typecheck
pnpm test
pnpm lint
pnpm buildSubmit a PR! 🎉
- Code compiles (
pnpm build) - Tests pass (
pnpm test) - Lint passes (
pnpm lint) - TypeScript checks pass (
pnpm typecheck) - New functionality includes tests
- Command is registered in
src/commands/index.ts
- No external runtime dependencies — only
chalkis allowed. Everything else uses Node.js built-in APIs - ESM only — all imports must use the
.jsextension (e.g.import { foo } from './bar.js') - Error handling — throw
ExitErroror useexitWithError(), never callprocess.exit() --jsonsupport — all commands should support--jsonfor machine-readable output- Pipe support — accept input from
stdinviareadStdinSync()when no argument is provided - Interactive mode — if your command has custom prompts, add a handler in
src/interactive.ts
Open an issue at https://github.com/yvng-jie/devtools-cli/issues