@jshow/cli
v1.0.11
Published
A simple CLI tool for j-show
Readme
Overview
@jshow/cli is a command line tool set for jShow, developed based on commander. It provides a powerful and extensible CLI framework with automatic command discovery, plugin system, and full TypeScript support.
The CLI automatically scans and loads command files (.cmd.ts or .cmd.js) and plugin files (.plugin.ts or .plugin.js) from your project, making it easy to build custom CLI tools with minimal configuration.
Features
| Feature | Description |
| --- | --- |
| Auto Command Discovery | Automatically scans and loads command files (.cmd.ts or .cmd.js) in the project |
| Plugin System | Support for plugins with lifecycle hooks (beforeExecute, afterExecute) |
| Command Registration | Support custom command registration for easy command extension |
| Type Safety | Full TypeScript type support |
| Commander Export | Exports the entire commander library for direct use |
| Command Base Class | Provides BaseCommand abstract base class to simplify command development |
| Command Grouping | Organize commands into groups for better help information display |
| Workspace utilities | Re-exported helpers (getGroupPackages, Git wrappers, execSync, etc.) for release/backup flows and your own tooling |
Requirements
- Node.js: 18+ recommended (library targets modern Node with ESM).
- pnpm:
>=10when working in this repository (package.jsonengines). - TypeScript: Optional at runtime;
bin/cli.mjsloadsts-node/esmso.cmd.ts/.plugin.tsin the consumer project can be discovered.
Why @jshow/cli?
- Zero Configuration – Automatically discovers and loads commands/plugins from your project without manual registration.
- Type-Safe – Full TypeScript support with comprehensive type definitions for commands, plugins, and options.
- Extensible – Plugin system allows you to add cross-cutting concerns like logging, timing, and error handling.
- Commander Integration – Built on top of Commander.js, exports the entire library for advanced use cases.
- Developer Friendly – Simple base classes and clear conventions make it easy to create and maintain commands.
- Modern Tooling – Built for modern Node.js with ESM support and TypeScript-first design.
Quick Start
Install dependencies
pnpm add @jshow/cli # or npm install @jshow/cli # or yarn add @jshow/cliCreate a command file
Create a file ending with
.cmd.tsor.cmd.jsin your project:// example.cmd.ts import { BaseCommand, type CommandContext } from '@jshow/cli'; export default class ExampleCommand extends BaseCommand { static name = 'example'; static force = false; public get args() { return { name: 'example', description: 'This is an example command', aliases: ['ex', 'e'], group: 'examples', plugins: ['logger', 'timer'], // Optional: specify plugins to use options: [ { name: 'name', abbr: 'n', flagValue: true, description: 'Name parameter', defaultValue: 'world', required: false, }, ], examples: [ 'jshow example', 'jshow example --name "jshow"', 'jshow ex -n "test"', ], validate: (options) => { // Optional: custom validation if (options.name && typeof options.name !== 'string') { return 'Name must be a string'; } return null; }, }; } public async beforeExecute(context: CommandContext): Promise<void> { console.log(`Starting command: ${context.name}`); } public async execute(context: CommandContext): Promise<void> { const { options } = context; console.log(`Hello, ${String(options.name || 'world')}!`); } public async afterExecute(context: CommandContext): Promise<void> { console.log(`Command completed in ${Date.now() - context.startTime}ms`); } }Run the command
# Development (runs src/cli.ts via ts-node from package root) pnpm start # Production-style: build then run the published bin (loads dist/cli.mjs with ts-node loader when needed) pnpm build pnpm exec jshow example --name "jshow" # or, if jshow is on PATH: jshow example --name "jshow"
Library usage
Compose the framework in your own Node process without cwd auto-discovery:
import { CommandProgram, initBuiltIn, BaseCommand, type CommandContext } from '@jshow/cli';
class DeployCommand extends BaseCommand {
static key = 'deploy';
protected get args() {
return { name: 'deploy', description: 'Deploy service' };
}
async execute(ctx: CommandContext) {
console.log(ctx.options);
}
}
CommandProgram.use(DeployCommand);
await initBuiltIn(CommandProgram).run();- The package entry also re-exports all of
commander,./utils, andlogger(see Utilities below). runjShow/dist/cli.mjsare not exported from the main entry; use thebinor the built CLI module for full discovery.
Repository scripts
| Command | Description |
| --- | --- |
| pnpm build | Cleans dist/ / out/ then runs vite build (library + cli entry as dist/*.mjs / *.cjs) |
| pnpm test | Runs vitest --run |
| pnpm start | cd src && ts-node ./cli.ts — dev CLI against the current working directory |
| pnpm cli | Runs bin/cli.mjs from test/fixtures/empty-cli-cwd (e.g. pnpm cli -- --help) |
| pnpm clean | rm -rf ./dist && rm -rf ./out (Unix); on Windows use manual removal or Git Bash if rm is unavailable |
| pnpm fix:all | Prettier + ESLint fix |
Environment variables
| Variable | Where used | Description |
| --- | --- | --- |
| JSHOW_CLI_MAX_DEPTH | src/cli.ts | Max directory depth when scanning for .cmd / .plugin files under process.cwd() (parsed as integer, minimum 2). |
| JSHOW_CLI_IGNORE_NAMES | src/cli.ts | Comma-separated top-level directory names to skip while scanning (e.g. other packages in a monorepo). |
| JSHOW_CLI_TS_RUNTIME | src/cli.ts | Set to 1 to treat the process as ts-node-capable and allow loading .cmd.ts / .plugin.ts during discovery. |
| JSHOW_CLI_NO_TS_LOADER | bin/cli.mjs | Set to 1 to run dist/cli.mjs without the ts-node ESM loader (.ts discovery files are not loaded). |
| TS_NODE_PROJECT / TS_NODE_COMPILER_OPTIONS / execArgv with ts-node | src/cli.ts, bin/cli.mjs | When set (or when the bin uses the ts-node loader), .ts discovery files may be loaded; otherwise only .js files are loaded from the workspace. |
Examples
The examples/ directory contains working examples:
TypeScript Examples
hello.cmd.ts– A simple Hello World command demonstrating basic command structuregreet.cmd.ts– A command with options, aliases, and validationbuild.cmd.ts– A complex command using plugins and command grouping
CommonJS Examples
hello.cmd.js– A basic command example using CommonJS syntaxbuild.cmd.js– A command with plugins example using CommonJS syntax
Plugin Examples
logger.plugin.ts– A logging plugin with lifecycle hooks (priority: 50)timer.plugin.ts– A timing plugin for performance monitoring (priority: 100)error-handler.plugin.ts– An error handling plugin example (priority: 200)
See examples/README.md for detailed usage instructions.
Built-in commands
Registered automatically by initBuiltIn(CommandProgram) before CommandProgram.run():
release: interactive flow to pick public packages, bump versions (semver), runpnpm install,git add/git commit -F, and optionalgit push(multi-repo and monorepo can run in one invocation). End-of-run Report table includesstatusandcount(selected packages). Seedocs/release.md.publish: validates a single package, stripsdevDependencies, resolvesworkspace:/catalog:versions for publish, and runsnpm publish(CI-oriented; leaves the formattedpackage.jsonon disk). Seedocs/publish.md.backup: resolves packages viagetGroupPackages(falls back to.gitrepo scan), optionallygit pullper package, then copies each package’s top-level entries to an output folder (skipsnode_modules;-cexcludes.git). Seedocs/backup.md.upgrade: scans workspace dependencies, lets you multi-select dependencies to query, fetches registry versions, interactively confirms per-field bumps, writespackage.json, runspnpm install, and optional Git commit/push (multi-repo vs monorepo cannot be mixed).--forceskips commit/push prompts. Seedocs/upgrade.md(--localnot wired yet).
Implementations: src/built-in/commands/.
Utilities (re-exported utils)
Imported from @jshow/cli, shared with built-in release / backup:
| Area | Symbols | Purpose |
| --- | --- | --- |
| Workspace | getGroupPackages, getWorkspacePackages, separateGroupPackages | Scan monorepo / multi-package roots |
| FS / process | existsSync, readJsonSync, writeJsonSync, execSync, cpSync, … | Safe I/O and sync subprocess |
| Git | getCurrentBranch, pullCurrentBranch, getUnCommittedFiles, diffGit, addGit, commitGit, pushGit, … | Release/upgrade/backup Git helpers |
| pnpm | installPnpm, readPnpmCatalogs, findPnpmWorkspaceRoot, PNPM_BUILT_IN_WORKSPACE, PNPM_BUILT_IN_CATALOG | Install deps, catalog read, workspace root lookup, built-in prefixes |
| Prompts | confirmInquirer, inputInquirer, checkboxInquirer, … | Dynamic inquirer wrappers for CLI |
| Terminal | red, green, yellow | ANSI color helpers |
| Regexp | toRegExp, toPatterns | Comma-separated filters (e.g. backup -f, upgrade -i) |
See src/utils/index.ts and submodule JSDoc for the full surface.
API Documentation
Package entry (@jshow/cli)
The published "." export includes: everything from commander, CommandProgram, initBuiltIn, BaseCommand / BasePlugin and related types, isCommand / isPlugin, all symbols from ./utils, and the shared logger. The runnable CLI is the separate build target dist/cli.mjs (wired via bin/cli.mjs); it is not re-exported from the main entry to avoid importing the package accidentally starting a CLI.
CommandProgram
Singleton-style facade: holds the Commander root program, plugin list, and command registry.
Static properties
version: string— read from the package’s ownpackage.jsonnext to the builtprogrammodule.program: Command— Commander root; subcommands are mounted here inrun().
Static methods
use(command: CommandClassType, force?: boolean): typeof CommandProgram
Registers a command class. Registration key is command.key if set, otherwise command.name (note: static name = 'foo' overrides Function.name).
install(plugin: PluginClassType, force?: boolean): typeof CommandProgram
Installs a plugin class; instances are sorted by ascending priority (smaller runs earlier).
reset(autoRun?: boolean): void
Clears plugins/commands and rebuilds the root Command (intended for tests; optional autoRun re-inits built-ins).
run(): Promise<void>
Mounts all commands, enhances help text, then await program.parseAsync(process.argv).
initBuiltIn
initBuiltIn(CommandProgram) installs default plugins and registers built-in commands (release, publish, backup, upgrade), returning CommandProgram for chaining.
CommandArgs
Command argument configuration interface.
Properties
name: string- Command name (required)description?: string- Command descriptionaliases?: string[]- Command aliasesplugins?: string[]- List of plugin names to use for this commandgroup?: string- Command group for help organizationarguments?: CommandArgument[]— positional arguments (command.argument(...))options?: CommandOption[]— Commander options (command.option(...))examples?: string[]- Usage examplesvalidate?: (options: Record<string, unknown>) => string | null- Optional validation function that returns an error message or null
CommandOption
Command option configuration interface.
Properties
name: string- Option long name (used as--${name}), e.g.'name'or'verbose'abbr?: string- Option short name (single char), e.g.'n'for-nflagValue?: boolean- Whentrue, the option is declared with a value placeholder (--name <arg>style); whenfalse, it is a boolean flagdescription?: string- Option descriptiondefaultValue?: T- Default value for the optionrequired?: boolean- Whether the option is required (default:false)variadic?: boolean- Whether this option accepts multiple values (becomes--name <value...>)
BaseCommand
Command base class. All custom commands should extend this class.
Static properties
static key: string— preferred registration key (defaults to''; may be filled from filename when auto-loading).static force: boolean— allow replacing an existing registration (defaultfalse).static name = 'subcommand'— optional; overridesFunction.nameand can be used as the registration key whenkeyis empty.
Instance properties
key(getter): resolvesstatic key, else constructorname, elseargs.name.command: Command- Commander command instance (protected)
Abstract Methods
execute(context: CommandContext): Promise<void>
Command execution logic. Subclasses must implement this method.
Protected Methods
get args(): CommandArgs
Get command argument configuration. Subclasses must implement this getter.
The CommandArgs interface includes:
name: string- Command namedescription?: string- Command descriptionaliases?: string[]- Command aliasesplugins?: string[]- List of plugin names to use for this commandgroup?: string- Command group for help organizationarguments?/options?— as aboveexamples?: string[]- Usage examplesvalidate?: (options: Record<string, unknown>) => string | null- Optional validation function
beforeExecute?(context: CommandContext): Promise<void>
Lifecycle hook executed before command execution.
afterExecute?(context: CommandContext): Promise<void>
Lifecycle hook executed after command execution.
onError(error: Error, context: CommandContext): boolean
Error handling hook. Returns true if error is handled, false otherwise.
BasePlugin
Plugin base class. All custom plugins should extend this class.
Static properties
static key: string— registration key (defaults to'').static force: boolean— allow replacing an existing plugin registration.
Instance properties
key(getter):static keyor constructorname.priority: number- Plugin priority (default: 100, lower number = higher priority)
Methods
beforeExecute?(context: CommandContext): Promise<void>
Lifecycle hook executed before command execution.
afterExecute?(context: CommandContext): Promise<void>
Lifecycle hook executed after command execution.
File Naming Conventions
Command Files
TypeScript Files
- File naming: Must end with
.cmd.ts - Default export: Must use
export defaultto export the command class - Extend base class: Command class must extend
BaseCommand - Static identity: Set
static keyand/orstatic name(used withCommandProgram.use); filename-derivedkeyis applied when auto-loading if missing - Implement methods: Must implement
execute(context)method andargsgetter
CommonJS Files
- File naming: Must end with
.cmd.js - Import dependencies: Use
require()to import:const { BaseCommand } = require('@jshow/cli'); - Export class: Use
module.exportsto export class (Node.js automatically treats it as default export) - Extend base class: Command class must extend
BaseCommand - Static identity: Set
static keyand/orstatic name - Implement methods: Must implement
execute(context)method andargsgetter
Plugin Files
TypeScript Files
- File naming: Must end with
.plugin.ts - Default export: Must use
export defaultto export the plugin class - Extend base class: Plugin class must extend
BasePlugin - Static identity: Set
static keyand/orstatic name
CommonJS Files
- File naming: Must end with
.plugin.js - Import dependencies: Use
require()to import:const { BasePlugin } = require('@jshow/cli'); - Export class: Use
module.exportsto export class (Node.js automatically treats it as default export) - Extend base class: Plugin class must extend
BasePlugin - Static identity: Set
static keyand/orstatic name
Auto Discovery
The CLI automatically scans the current working directory and its subdirectories, finds all .cmd.ts, .cmd.js, .plugin.ts, or .plugin.js files and loads them automatically.
Scanning rules
- Starts at
process.cwd()and recurses up toJSHOW_CLI_MAX_DEPTH(default2, minimum2). - Skips dot-prefixed dirs, common junk dirs (
node_modules, etc., seeisIgnoreDir), names listed inJSHOW_CLI_IGNORE_NAMES, and the package’s ownbuilt-in/commandstree when it appears under the scan path. - Only loads files ending with
.cmd.ts,.cmd.js,.plugin.ts, or.plugin.js. .tsfiles are only considered when a ts-node-style runtime is detected; otherwise only.jsfiles load.- If the class has no
static key, the basename (without.cmd/.plugin) is assigned tokeybefore registration.
Development notes
- Library vs CLI: import
@jshow/cliforCommandProgram/BaseCommand/utils; runjshow(orpnpm startin this repo) for cwd auto-discovery. Entry points:src/index.ts(library),src/cli.ts→dist/cli.mjs(CLI).runjShowis exported fromsrc/cli.tsfor tests/custom wrappers but not from the package main entry. - Discovery resilience: a broken
.cmd/.pluginin the workspace logs a warning only so--helpand built-ins still run (loadCommand/loadPlugin). - Boolean
invert: forflagValue: falseoptions,invert: truealso registers--no-<name>(used by built-inbackup -c,release --check/--push, etc.); seeinitOptioninsrc/command.ts. - Built-in commands: registered in
src/built-in/commands/index.ts; seedocs/*.mdfor flow details and this README for a summary. - JSDoc: public and internal helpers in
src/are documented next to implementations—prefer source JSDoc over duplicating API lists here.
Development Workflow
pnpm install– Install dependencies- Create command files (
.cmd.tsor.cmd.js) or plugin files (.plugin.tsor.plugin.js) in your project pnpm start– Run in development modepnpm build– Build for production- Test your commands with
jshow <command>
Directory Layout
├── bin/cli.mjs # Published bin: Node + ts-node loader → dist/cli.mjs
├── src/
│ ├── cli.ts # Runnable CLI (scan cwd, initBuiltIn, parseAsync)
│ ├── index.ts # Library entry (re-exports commander + framework + utils + logger)
│ ├── command.ts # BaseCommand & option/argument types
│ ├── plugin.ts # BasePlugin
│ ├── program.ts # CommandProgram, initBuiltIn
│ ├── logger.ts # Shared logger fork
│ ├── built-in/ # Default commands/plugins wired by initBuiltIn
│ └── utils/ # Workspace scan, git, pnpm, fs helpers
├── test/ # Vitest specs and fixtures (not published)
├── docs/ # Built-in command docs (backup / publish / release / upgrade)
├── examples/ # Sample .cmd / .plugin files
├── scripts/ # Dev helpers (e.g. run-cli-help.mjs)
├── dist/ # Vite build output (gitignored)
└── ...License
MIT © jShow
Questions or issues? Open an issue at https://github.com/j-show/cli/issues.
