Provides patterns for building TypeScript CLI tools distributed via npx - package structure, the bin entry point and shebang, ESM-ready tsconfig, argument parsing, output and exit codes, local testing, and npm publishing. Use when the user asks to "create an npx package", "build a CLI tool with TypeScript", "set up a Node.js CLI", "publish an npm package", "configure package.json bin field", "add CLI argument parsing", or "create an executable npm package", or when writing TypeScript code for a command-line tool distributed via npx. Does not cover general TypeScript style, type design, or non-CLI project layout - see the typescript skill for those.
74
93%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
Passed
No findings from the security scan
Patterns for TypeScript CLI tools that run via npx: a standard npm package with a bin entry point.
my-cli/
├── package.json
├── tsconfig.json
├── src/
│ ├── index.ts # entry point (bin target)
│ ├── cli.ts # argument parsing and command routing
│ └── commands/ # command implementations
│ └── install.ts
├── dist/ # compiled output (gitignored)
└── README.md{
"name": "my-cli",
"version": "1.0.0",
"description": "CLI tool description",
"type": "module",
"bin": {
"my-cli": "./dist/index.js"
},
"files": [
"dist"
],
"scripts": {
"build": "tsc",
"dev": "tsc --watch",
"prepublishOnly": "npm run build"
},
"engines": {
"node": ">=24"
},
"devDependencies": {
"typescript": "^7",
"@types/node": "^24"
}
}Set engines.node to the current Node LTS major and keep the @types/node major equal to it. The devDependency ranges above are examples — install with npm install -D typescript @types/node to get the latest majors rather than copying these pins.
Key fields:
| Field | Purpose |
|---|---|
name | Package name on npm. Use @scope/name for scoped packages |
bin | Maps command names to compiled JS entry points |
files | Whitelist of files included in the published package |
type: "module" | Enable ESM (import/export syntax) |
engines | Minimum Node.js version requirement |
prepublishOnly | Auto-builds before npm publish |
The entry point file must start with a Node.js shebang:
#!/usr/bin/env node
// src/index.ts
import { run } from "./cli.js";
run(process.argv.slice(2));Important: The shebang must be the very first line — no blank lines or comments above it — or npx execution fails on Unix.
{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"declaration": true,
"sourceMap": true
},
"include": ["src"],
"exclude": ["node_modules", "dist"]
}Key settings:
module: "Node16" + moduleResolution: "Node16" — required for proper ESM support in Node.js.outDir: "./dist" — compiled JS goes to dist/, matching the bin path in package.json.declaration: true — emit .d.ts files for consumers who import the package programmatically.ESM import rule: When using type: "module", all relative imports must include the .js extension, even in .ts source files:
// Correct
import { run } from "./cli.js";
// Wrong — will fail at runtime
import { run } from "./cli";For simple CLIs with 1-3 commands:
// src/cli.ts
export function run(args: string[]): void {
const command = args[0];
switch (command) {
case "install":
handleInstall(args.slice(1));
break;
case "search":
handleSearch(args.slice(1));
break;
case "--help":
case "-h":
printHelp();
break;
case "--version":
case "-v":
printVersion();
break;
default:
console.error(`Unknown command: ${command}`);
printHelp();
process.exit(1);
}
}function getFlag(args: string[], flag: string): string | undefined {
const index = args.indexOf(flag);
if (index === -1 || index + 1 >= args.length) return undefined;
return args[index + 1];
}
// Usage: npx my-cli install --name my-plugin
const name = getFlag(args, "--name");For CLIs with many commands, flags, and subcommands, use commander — see references/patterns.md.
Node.js supports ANSI escape codes natively:
const red = (s: string) => `\x1b[31m${s}\x1b[0m`;
const green = (s: string) => `\x1b[32m${s}\x1b[0m`;
const bold = (s: string) => `\x1b[1m${s}\x1b[0m`;
const dim = (s: string) => `\x1b[2m${s}\x1b[0m`;
console.log(green("✓ Installed successfully"));
console.error(red("✗ Installation failed"));process.exit(0); // success
process.exit(1); // general error
process.exit(2); // misuse (bad arguments)Always call process.exit() with the appropriate code on failure.
Program output (data, results) goes to stdout via console.log(); diagnostics (errors, warnings, progress) go to stderr via console.error().
# Build
npm run build
# Test locally (symlinks the package globally)
npm link
my-cli --help
# Or test with npx directly
npx . install --name my-plugin
# Unlink when done
npm unlink -g my-cli# Login (one-time)
npm login
# Publish (runs prepublishOnly → build automatically)
npm publish
# Publish scoped package as public
npm publish --access publicAfter publishing, the package is immediately available via:
npx my-cli install --name my-pluginnpm version patch # 1.0.0 → 1.0.1
npm version minor # 1.0.0 → 1.1.0
npm version major # 1.0.0 → 2.0.0Run npm version before npm publish — it updates package.json and creates a git tag.
| Mistake | Fix |
|---|---|
| Missing shebang | Add #!/usr/bin/env node as first line of entry point |
| Shebang not on first line | Remove blank lines/comments above #!/usr/bin/env node |
Missing .js in ESM imports | Add .js extension to all relative imports |
bin points to .ts file | Point to compiled .js in dist/ |
dist/ not in files | Add "files": ["dist"] to package.json |
No prepublishOnly script | Add "prepublishOnly": "npm run build" |
| File not executable on Unix | Run chmod +x dist/index.js (usually handled by npm) |
For detailed patterns and advanced configuration, consult:
provectus/awos-recruitment
ce6a3ea
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.