This guide details how to build command libraries and CLIs using the @nexical/cli-core framework within this project.
The project is configured to use @nexical/cli-core as its CLI framework. usage entails:
- Entry Point:
index.tsinitializes theCLIinstance. - Command Discovery: The CLI automatically scans
src/commandsfor command files. - Command Implementation: Commands are TypeScript classes extending
BaseCommand.
Commands are defined in src/commands. The file structure directly maps to the command hierarchy.
| File Path | Command | Description |
|---|---|---|
src/commands/init.ts |
app init |
Root level command |
src/commands/user/create.ts |
app user create |
Subcommand |
src/commands/user/index.ts |
app user |
Parent command handler (optional) |
Note: The CLI name (
app) is configured inindex.ts.
To create a new command, add a .ts file in src/commands.
Create src/commands/hello.ts:
import { BaseCommand } from '@nexical/cli-core';
export default class HelloCommand extends BaseCommand {
// Description displayed in help menus
static description = 'Prints a hello message';
// The main execution method
async run(options: any) {
this.success('Hello World!');
}
}Run it:
npm run cli helloUse the static args property to define inputs.
import { BaseCommand } from '@nexical/cli-core';
export default class GreetCommand extends BaseCommand {
static description = 'Greets a user';
static args = {
// Positional Arguments
args: [
{
name: 'name',
description: 'Name of the user',
required: true
}
],
// Options (Flags)
options: [
{
name: '--loud',
description: 'Print in uppercase',
default: false
},
{
name: '-c, --count <n>',
description: 'Number of times to greet',
default: 1
}
]
};
async run(options: any) {
// 'name' is mapped from args
// 'loud' and 'count' are mapped from options
const { name, loud, count } = options;
let message = `Hello, ${name}`;
if (loud) message = message.toUpperCase();
for(let i=0; i < count; i++) {
this.info(message);
}
}
}Run it:
npm run cli greet Adrian --loud --count 3BaseCommand provides built-in methods for interactivity.
export default class InteractiveCommand extends BaseCommand {
async run() {
// Simple confirmation or input
const name = await this.prompt('What is your name?');
this.success(`Nice to meet you, ${name}`);
}
}Use the built-in helper methods for consistent logging:
this.success(msg): Green checkmark (✔ Success)this.error(msg): Red cross (✖ Error) - Exits processthis.warn(msg): Yellow warning (⚠ Warning)this.info(msg): Standard logthis.notice(msg): Blue notice (📢 Note)this.input(msg): Cyan input prompt (? Question)
To create grouped commands (e.g., user create, user list), use directories.
- Create Directory:
src/commands/user/ - Add Commands:
src/commands/user/create.ts->app user createsrc/commands/user/list.ts->app user list
If you need the parent command app user to do something (or just provide a description for the group), create src/commands/user/index.ts.
// src/commands/user/index.ts
import { BaseCommand } from '@nexical/cli-core';
export default class UserCommand extends BaseCommand {
static description = 'Manage users';
async run() {
// This runs when user types 'app user' without a subcommand
// Often used to show help
this.cli.getRawCLI().outputHelp();
}
}All commands inherit from BaseCommand.
Properties:
cli: Access to the mainCLIinstance.projectRoot: Path to the project root (if detected).config: Loaded configuration from{cliName}.yml.globalOptions: Global flags passed to the CLI (e.g.,--debug).
Methods:
init(): Called beforerun(). Useful for setup.run(options): Abstract method. Must be implemented.prompt(message): Async, returns string.
Static Properties:
description: String. Shown in help.args: Object. Defines arguments and options.args: Array of{ name, description, required, default }.options: Array of{ name, description, default }.
requiresProject: Boolean. If true, command fails if not run inside a project with a config file.
- Type Safety: While
optionsisanyinrun(), validate inputs early. - Error Handling: Use
this.error()for fatal errors to ensure proper exit codes. - Clean Output: Use the helper methods (
success,info, etc.) instead ofconsole.logfor a consistent UI. - Async: The
runmethod is async. Alwaysawaitasynchronous operations.
- Command not found: Ensure the file exports a class leveraging
export defaultand extendsBaseCommand. - Changes not reflected: If using
tsup, ensure you are building or running in dev mode (npm run dev). Fornpm run cliusingts-node(viacli.tsor similar), changes should be instant.