Skip to main content

Commands

See how to create and configure your CLI commands.

Introduction​

In Athenna you are able to create your own CLI application creating your own commands by extending the abstract class Command and implement its methods.

Writing commands​

In addition to the commands provided with Artisan, you may build your own custom commands. Commands are typically stored in the app/console/commands directory; however, you are free to choose your own storage location as long as your commands can be imported and registered.

To create a new command, you may use the make:command Artisan command. This command will create a new command class in the app/console/commands directory and register it inside commands object of .athennarc.json file. Don't worry if this directory does not exist in your applicationβ€”it will be created the first time you run the make:command Artisan command:

node artisan make:command SendEmails

Registering commands​

All of your console commands are registered within your application's .athennarc.json file. If you have created your command using make:command your command will already be registered for you in commands object, but we still recommend you to do some adjustments before using your command.

In the example above we have changed the signature of SendEmails command to send:email, we should always use the command signature as key when registering our commands in commands object:

{
"commands": {
"send:email": { πŸ‘ˆ
"path": "#app/console/commands/SendEmails",
"loadApp": true
}
}
}

If you do so, Athenna will always import and load only that specific command that you want to execute, meaning that if you execute the following command:

node artisan send:email lenon@athenna.io

Athenna will only import and load SendEmails class and execute your command. If you change the key name to any value different from the command signature (send:email) Athenna will load all your commands defined in commands object:

{
"commands": {
"sendEmails": { πŸ‘ˆ // All commands will be loaded
"path": "#app/console/commands/SendEmails",
"loadApp": true
}
}
}

Loading all commands might be useful sometimes when your command wants to execute other commands in runtime inside your command:

import { Inject } from '@athenna/ioc'
import { MailgunService } from '#app/services/MailgunService'
import { Option, Artisan, Argument, BaseCommand } from '@athenna/artisan'

export class SendEmails extends BaseCommand {
public static signature(): string {
return 'mail:send'
}

public static description(): string {
return 'Send an email.'
}

@Argument()
private email: string

@Option({
signature: '-s, --subject <subject>',
default: 'Athenna',
description: 'Set the subject of the email.',
})
private subject: string

@Inject()
private mailgunService: MailgunService

public async handle(): Promise<void> {
const message = 'Hello from Athenna!'

await this.mailgunService.send(this.email, {
message,
subject: this.subject,
})

await Artisan.call(`save:email ${this.email}`)
}
}
tip

Even for this purpose, we recommend you to let the key of your command the same of your command signature. To load all your commands, use the loadAllCommands setting in your command:

{
"commands": {
"send:email": {
"path": "#app/console/commands/SendEmails",
"loadApp": true,
"loadAllCommands": true πŸ‘ˆ // All commands will be loaded
}
}
}

Commands structure​

Athenna commands are represented as classes and always extend the BaseCommand class. You define the command signature, description and commander as static properties on the class itself:

import { BaseCommand, type Commander } from '@athenna/artisan'

export class SendEmails extends BaseCommand {
public static signature(): string {
return 'sendEmails'
}

public static description(): string {
return 'The description of sendEmails command'
}

public static commander(commander: Commander): Commander {
return commander
}

public async handle(): Promise<void> {
//
}
}
  • signature - The signature is basicaly the command. You will always use this signature to call your command in terminal. It should always be a string.
  • description - The description is a short description of what your command does, it will always be displayed in the help output.
  • commander - The commander method should be used when you want to set customized options for commander.
  • handle - The handle method will contain the logic of your command, this method will be called when executing your command.

Booting the app within the command​

Artisan commands do not boot your application before running the command. If your command relies on the application code, you must instruct the command to load the application first and then execute the handle method:

.athennarc.json
{
"commands": {
"sendEmails": {
"path": "#app/console/commands/SendEmails",
"loadApp": true πŸ‘ˆ
}
}
}

Now we are able to request any dependencies we need using the @Inject() annotation:

import { Inject } from '@athenna/ioc'
import { BaseCommand } from '@athenna/artisan'
import { MailgunService } from '#app/services/MailgunService'

export class SendEmails extends BaseCommand {
public static signature(): string {
return 'mail:send'
}

public static description(): string {
return 'Send an email.'
}

@Inject()
private mailgunService: MailgunService πŸ‘ˆ

public async handle(): Promise<void> {
const msg = 'People reading this will have a wonderful day! πŸ₯³'

await this.mailgunService.send(msg)
}
}

Constructor injection is not allowed​

The application is bootstrapped only after your command is already registered, meaning that you can't use the automatic constructor injection with commands to resolve your dependencies:

❌ Does not work​

import { BaseCommand } from '@athenna/artisan'
import { MailgunService } from '#app/services/MailgunService'

export class SendEmails extends BaseCommand {
public static signature(): string {
return 'mail:send'
}

public static description(): string {
return 'Send an email.'
}

public constructor(private mailgunService: MailgunService) { πŸ‘ˆ
super()
}

public async handle(): Promise<void> {
const msg = 'People reading this will have a wonderful day! πŸ₯³'

await this.mailgunService.send(msg)
}
}

βœ… Works​

import { BaseCommand } from '@athenna/artisan'
import type { MailgunService } from '#app/services/MailgunService'

export class SendEmails extends BaseCommand {
public static signature(): string {
return 'mail:send'
}

public static description(): string {
return 'Send an email.'
}

public async handle(): Promise<void> {
const mailgunService = ioc.safeUse<MailgunService>('App/Services/MailgunService')

const msg = 'People reading this will have a wonderful day! πŸ₯³'

await mailgunService.send(msg)
}
}
tip

For greater code reuse, it is good practice keeping your console commands light and let them defer to application services to accomplish their tasks. In the example above, note that we inject a service class to do the "heavy lifting" of sending the e-mails.

Defining input expectations​

When writing console commands, it is common to gather input from the user through arguments or options. Athenna makes it very convenient to define the input you expect from the user using the @Argument() and @Option() annotations on your commands. See the example:

import { Inject } from '@athenna/ioc'
import { MailgunService } from '#app/services/MailgunService'
import { Option, Argument, BaseCommand } from '@athenna/artisan'

export class SendEmails extends BaseCommand {
public static signature(): string {
return 'mail:send'
}

public static description(): string {
return 'Send an email.'
}

@Argument()
private email: string

@Option({
signature: '-s, --subject <subject>',
default: 'Athenna',
description: 'Set the subject of the email.',
})
private subject: string

@Inject()
private mailgunService: MailgunService

public async handle(): Promise<void> {
const message = 'Hello from Athenna!'

await this.mailgunService.send(this.email, {
message,
subject: this.subject,
})
}
}

Arguments​

Command arguments are positional, and they are accepted in the same order as you define them in your class. For example:

import { Argument, BaseCommand } from '@athenna/artisan'

export class Greet extends BaseCommand {
public static signature(): string {
return 'greet'
}

@Argument()
public name: string

@Argument()
public age: string
}
node artisan greet <name> <age>

Spread/Variadic arguments​

The @Argument() annotation allows you to define a catch-all argument. It is like the rest parameters in JavaScript and must always be the last argument:

import { Argument, BaseCommand } from '@athenna/artisan'

export class Greet extends BaseCommand {
public static signature(): string {
return 'greet'
}

@Argument()
public age: string

@Argument({ signature: '<names...>' }) πŸ‘ˆ
public names: string[] πŸ‘ˆ

public async handle() {
console.log(this.age, this.names)
}
}
node artisan greet 22 lenon txsoura

Will output:

22 [ 'lenon', 'txsoura' ]

Arguments options​

To check all the available options for the @Argument() annotation and see details arround them, check the @Argument() annotation documentation section.

Options​

You define the options using the @Option() annotation:

import { Option, BaseCommand } from '@athenna/artisan'

export class Greet extends BaseCommand {
public static signature(): string {
return 'greet'
}

@Option({ signature: '-n, --name <name>' })
public name: string

public async handle() {
console.log('hey', this.name)
}
}
node artisan greet -n lenon

Will output:

hey lenon

Negatable boolean options​

You can define a boolean option long name with a leading no- to set the option value to false when used. Defined alone this also makes the option true by default.

If you define --foo first, adding --no-foo does not change the default value from what it would otherwise be:

import { Option, BaseCommand } from '@athenna/artisan'

export class AthennaPizzeria extends BaseCommand {
public static signature(): string {
return 'pizza'
}

@Option({ signature: '--no-sauce' })
public addSauce: boolean

@Option({ signature: '--cheese <flavour>' })
public cheeseFlavour: string

@Option({ signature: '--no-cheese' })
public addCheese: boolean

public async handle() {
const sauceStr = this.addSauce ? 'sauce' : 'no sauce'
const cheeseStr = (this.addCheese === false) ? 'no cheese' : `${this.chesseFlavour} cheese`;

console.log(`You ordered a pizza with ${sauceStr} and ${cheeseStr}`);
}
}
node artisan pizza
You ordered a pizza with sauce and mozzarella cheese

node artisan pizza --sauce
error: unknown option '--sauce'

node artisan pizza --cheese=blue
You ordered a pizza with sauce and blue cheese

node artisan pizza --no-sauce --no-cheese
You ordered a pizza with no sauce and no cheese

Spread/Variadic options​

The @Argument() annotation allows you to define a catch-all argument. It is like the rest parameters in JavaScript and must always be the last argument:

import { Option, BaseCommand } from '@athenna/artisan'

export class Greet extends BaseCommand {
public static signature(): string {
return 'greet'
}

@Option({ signature: '--names <names...>' }) πŸ‘ˆ
public names: string[] πŸ‘ˆ

public async handle() {
console.log(this.names)
}
}
node artisan greet --names=lenon --names=txsoura

Will output:

[ 'lenon', 'txsoura' ]

Options options​

To check all the available options for the @Option() annotation and see details arround them, check the @Option() annotation documentation section.

Prompts​

Artisan has support for creating interactive prompts on the terminal thanks to inquirer. You can access the prompts module using the this.prompt property.

In the example below, we are using multiple prompts together:

import { BaseCommand } from '@athenna/artisan'

export class Greet extends BaseCommand {
public static signature(): string {
return 'greet'
}

public async handle() {
const name = await this.prompt.input('What is your name?')
const password = await this.prompt.secret('What is your password?')
const isRobot = await this.prompt.confirm('Are you a robot?')
const code = await this.prompt.editor('Write a hello world code')
const card = await this.prompt.list('Select your card type', [
'debit',
'credit',
])
const roles = await this.prompt.checkbox('Select the roles', [
'customer',
'admin',
])

console.log()

this.logger.success('Information successfully collected:')
this.logger
.table()
.head('Names', 'Password', 'Is Robot?', 'Code', 'Card', 'Roles')
.row([name, password, `${isRobot}`, code, card, roles.join(', ')])
.render()
}
}
this.prompt.input()​

Displays the prompt to enter a input value. Optionally accepts options as the second argument:

await this.prompt.input('What is your name?', {
validate(input: string) {
if (!input || input.length < 2) {
return 'Name is required and must be over 2 characters'
}

return true
},
})

this.prompt.secret()​

Same as input() but hide what the user is typing. Optionally accepts options as the second argument:

await this.prompt.secret('What is your password?', {
validate(input: string) {
if (!input || input.length < 2) {
return 'Password is required and must be over 2 characters'
}

return true
},
})

this.prompt.confirm()​

Display the prompt to select between Yes and No. Optionally accepts options as the second argument:

await this.prompt.confirm('Are you a robot?')

this.prompt.editor()​

Open a code editor to write a bigger message, usually code. Optionally accepts options as the second argument:

await this.prompt.editor('Write a hello world code in Python')

this.prompt.list()​

Display a list of options with the possibility to choose only one. Optionally accepts options as the third argument:

await this.prompt.list('Select your registry', ['npm', 'pnpm', 'yarn'])

this.prompt.checkbox()​

Display a list of options with the possibility to choose multiple. Optionally accepts options as the third argument:

await this.prompt.checkbox('Select your dependencies', [
'@athenna/core', '@athenna/artisan', '@athenna/http'
])

Logger and UI​

You can make use of the inbuilt logger to log messages to the console using the this.logger property. Artisan logger extends the Logger class from @athenna/logger, but it also adds some new methods to it:

import { Exec } from '@athenna/common'
import { BaseCommand } from '@athenna/artisan'

export class Greet extends BaseCommand {
public static signature(): string {
return 'greet'
}

public async handle() {
this.logger.simple('({ green, bold }) Hello ({ yellow, bold, italic }) World!')

this.logger.update('Hey!!')
await Exec.sleep(1000)
this.logger.update('How you doing?')

this.logger.rainbow('Athenna Framework')

const spinner = this.logger.spinner('Loading unicorns')

spinner.start()

setTimeout(() => {
spinner.color = 'yellow'
spinner.text = 'Loading rainbows'
}, 1000)

await this.logger.promiseSpinner(Exec.sleep(2000), {
text: 'Loading',
successText: 'Success!',
failText: 'Failed!',
})

this.logger
.table()
.head('HEAD 1', 'HEAD 2')
.row(['VALUE 1', 'VALUE 2'])
.row(['VALUE 1', 'VALUE 2'])
.render()

const data = {
'commander@0.6.1': 1,
'minimatch@0.2.14': 3,
'mkdirp@0.3.5': 2,
'sigmund@1.0.0': 3,
}

this.logger.column(data, { columns: ['MODULE', 'COUNT'] })

const path = 'app/services/Service.ts'
const action = this.logger.action('create')

action.succeeded(path)
action.skipped(path, 'File already exists')
action.failed(path, 'Something went wrong')

this.logger
.instruction()
.head('Project Created')
.add(`cd ${this.paint.cyan('hello-world')}`)
.add(`Run ${this.paint.cyan('node artisan serve --watch')}`)
.render()

this.logger
.sticker()
.head('Athenna Framework')
.add('Follow us on Instagram: @athenna.io 🍷')
.render()

await this.logger
.task()
.add('First task', async task => {
await Exec.sleep(1000)
await task.complete('Completed!')
})
.add('Second task', async task => {
await Exec.sleep(1000)
await task.fail('Failed!')
})
.run()
}
}

this.logger.simple()​

Works like console.log(), but automatically handles the color engine:

this.logger.simple('({ green, bold }) Hello ({ yellow, bold, italic }) World!')

this.logger.update()​

Works like this.logger.simple(), but if you call the method multiple times, it will always update the message instead of printing a new one:

import { Exec } from '@athenna/common' 

this.logger.update('Hey!!')

await Exec.sleep(1000)

this.logger.update('How you doing?')

this.logger.rainbow()​

Print a message with all the colors of the rainbow and with FIGfont:

this.logger.rainbow('Hello World!')

this.logger.spinner()​

Creates a spinner to show a loading message. Very useful when running something in background and you want to give some feedback for your user:

const spinner = this.logger.spinner('Loading unicorns')

spinner.start()

setTimeout(() => {
spinner.color = 'yellow'
spinner.text = 'Loading rainbows'
}, 1000)

this.logger.promiseSpinner()​

Creates a spinner from a promise. If the promisse resolves show the successText, if fails, the failText, keep running the spinner until the task is running:

import { Exec } from '@athenna/common'

await this.logger.promiseSpinner(Exec.sleep(2000), {
text: 'Loading',
successText: 'Success!',
failText: 'Failed!',
})

this.logger.table()​

Create text-based table suitable for console output. To render the table you need to call the render() method at the end:

const table = this.logger.table()

this.logger.head('HEAD 1', 'HEAD 2')

this.logger.row(['VALUE 1', 'VALUE 2'])
this.logger.row(['VALUE 1', 'VALUE 2'])

this.logger.render()

this.logger.column()​

Create text-based columns suitable for console output from objects or arrays of objects:

const data = {
'commander@0.6.1': 1,
'minimatch@0.2.14': 3,
'mkdirp@0.3.5': 2,
'sigmund@1.0.0': 3,
}

this.logger.column(data, { columns: ['MODULE', 'COUNT'] })

this.logger.action()​

Log an action that is happening. Actions has three states succeeded, skipped and failed. You can use actions to explain to the user what happened to some determined operation:

const path = 'app/services/Service.ts'
const action = this.logger.action('create')

action.succeeded(path)
action.skipped(path, 'File already exists')
action.failed(path, 'Something went wrong')

this.logger.instruction()​

Use to display instructions for a given action by rendering them inside a box:

const instruction = this.logger.instruction()

instruction.head('Project Created')

instruction.add(`cd ${this.paint.cyan('hello-world')}`)
instruction.add(`Run ${this.paint.cyan('node artisan serve --watch')}`)

instruction.render()

this.logger.sticker()​

Use to display stickers for a given action by rendering them inside a box. Stickers have the same API of this.logger.instructions(), but the rows will not have the icon in front:

const sticker = this.logger.sticker()

sticker.head('Athenna Framework')
sticket.add('Follow us on Instagram: @athenna.io 🍷')

sticker.render()

this.logger.task()​

Create a task runner that will log the status of each task. The task method is beneficial when you need to do a lot of tasks in order, giving a status to all the tasks and how much time it has taken to execute:

import { Exec } from '@athenna/common'

const task = this.logger.task()

task.add('First task', async task => {
await Exec.sleep(1000)
await task.complete('Completed!')
})

task.add('Second task', async task => {
await Exec.sleep(1000)
await task.fail('Failed!')
})

await task.run()

You can also use the task.addPromise() method to don't mind about calling task.complete() or task.fail() method. This method will automatically call it for you if the promise resolves or rejects:

import { Exec } from '@athenna/common'

const task = this.logger.task()

task.addPromise('First task', async () => {
await Exec.sleep(1000)
})

task.addPromise('Second task', async () => {
await Exec.sleep(1000)
})

await task.run()

Generating templates in commands​

Artisan has support to generate files from templates. Let's create a command that generate repositories. First of all we need to create our template file and give it a name:

Path.resources('templates/repository.edge')
export class {{ namePascal }} {
}

Now we map our new template in .athennarc.json:

.athennarc.json
{
"templates": {
...
"repository": "./resources/templates/repository.edge"
...
}
}

And now we can run node artisan make:command MakeRepositoryCommand to create our command that will be responsible to create our repositories:

Path.commands('MakeRepositoryCommand.ts')
import { Path, String } from '@athenna/common'
import { sep, resolve, isAbsolute } from 'node:path'
import { BaseCommand, Argument } from '@athenna/artisan'

export class MakeRepositoryCommand extends BaseCommand {
@Argument({
description: 'The repository name.',
})
public name: string

public static signature(): string {
return 'make:repository'
}

public static description(): string {
return 'Make a new repository file.'
}

public async handle() {
this.logger.simple('({bold,green} [ MAKING REPOSITORY ])\n')

const file = await this.generator
.path(this.getFilePath())
.template('repository') // πŸ‘ˆ Our template name
.setNameProperties(true)
.make()

this.logger.success(
`Repository ({yellow} "${file.name}") successfully created.`,
)
}

private getFilePath(): string {
return this.getDestinationPath().concat(`${sep}${this.name}.${Path.ext()}`)
}

private getDestinationPath(): string {
let destination = Config.get(
'rc.commands.make:repository.destination',
Path.repositories(),
)

if (!isAbsolute(destination)) {
destination = resolve(Path.pwd(), destination)
}

return destination
}
}
tip

The example above is a little bit complex. But that is exactly what we do internally with other make:... commands.

this.generator.fileName()​

Set the name of the file and also the name that will be used used as reference to create the custom properties to be used in the template:

this.generator.fileName('UserRepository.ts')

this.generator.destination()​

Set the destination where the file should be created:

this.generator.destination(Path.repositories())

this.generator.extension()​

Set the extension of your file:

this.generator.extension('ts')

this.generator.path()​

Set the file path where the file will be generated. The fileName(), destination() and extension() will be ignored if a full path is set, meaning that when you set a path, the file name in the path will be used to define the name properties:

this.generator.path(Path.repositories('UserRepository.ts'))

this.generator.template()​

Set the template name to be resolved by the @athenna/view package that will read the templates property of .athennarc.json to find the path to your template file:

this.generator.template('repository')

this.generator.properties()​

Set custom properties names to be replaceble inside the template, check the example:

this.generator.properties({ name: 'Lenon' })
console.log('Hello {{ name }}')

this.generator.setNameProperties()​

Define custom properties in different cases from the file name:

this.generator.setNameProperties(true)

If you set this to true, the following properties will be available to be replaced in your template:

{
nameUp: 'MYREPOSITORY',
nameCamel: 'myRepository',
namePlural: 'MyRepositories',
namePascal: 'MyRepository',
namePluralCamel: 'myRepositories',
namePluralPascal: 'MyRepositories',
nameUpTimestamp: 'MYREPOSITORY1675363499530',
nameCamelTimestamp: 'myRepository1675363499530',
namePluralTimestamp: 'MyRepositories1675363499530',
namePascalTimestamp: 'MyRepository1675363499530',
namePluralCamelTimestamp: 'myRepositories1675363499530',
namePluralPascalTimestamp: 'MyRepositories1675363499530',
...properties, // <- Custom properties set with `this.generator.properties()` method.
}

this.generator.make()​

Finally, generate the file and return it as an instance of File. This method needs to always be called at the end to generate the file:

const file = await this.generator.make()

Manipulating .athennarc.json in commands​

You can manipulate the .athennarc.json file in your commands logic using the this.rc property. Let's suppose that in MakeRepositoryCommand we would like to register the repositories as a service, we could do the following:

import { Path, String } from '@athenna/common'
import { sep, resolve, isAbsolute } from 'node:path'
import { BaseCommand, Argument } from '@athenna/artisan'

export class MakeRepositoryCommand extends BaseCommand {
@Argument({
description: 'The repository name.',
})
public name: string

public static signature(): string {
return 'make:repository'
}

public static description(): string {
return 'Make a new repository file.'
}

public async handle() {
this.logger.simple('({bold,green} [ MAKING REPOSITORY ])\n')

const file = await this.generator
.path(this.getFilePath())
.template('repository')
.setNameProperties(true)
.make()

this.logger.success(
`Repository ({yellow} "${file.name}") successfully created.`,
)

const importPath = this.getImportPath(file.name)

await this.rc.pushTo('services', importPath).save() // πŸ‘ˆ

this.logger.success(
`Athenna RC updated: ({dim,yellow} { services += "${signature}": "${importPath}" })`,
)
}

private getFilePath(): string {
return this.getDestinationPath().concat(`${sep}${this.name}.${Path.ext()}`)
}

private getDestinationPath(): string {
let destination = Config.get(
'rc.commands.make:repository.destination',
Path.repositories(),
)

if (!isAbsolute(destination)) {
destination = resolve(Path.pwd(), destination)
}

return destination
}

private getImportPath(fileName: string): string {
const destination = this.getDestinationPath()

return `${destination
.replace(Path.pwd(), '')
.replace(/\\/g, '/')
.replace('/', '#')}/${fileName}`
}
}
tip

Again, the example above is a little bit complex. But that is exactly what we do internally with other make:... commands where we need to register something in the .athennarc.json file.

this.rc.file​

Manipulate the .athennarc.json File instance:

Log.info(this.rc.file.name)

this.rc.content​

Manipulate the content of .athennarc.json as an ObjectBuilder instance:

this.rc.content.has('commands') // true

this.rc.setFile()​

Specify which file should be manipulated. By default it will always be the .athennarc.json, if the file doesn't exist, the athenna property of package.json will be used:

this.rc.setFile(Path.pwd('.athennarc.prod.json'))

this.rc.setTo()​

Set or subscibre a KEY:VALUE property in some property of the RC configuration file.

const rcKey = 'commands'
const key = 'make:repository'
const value = '#app/console/commands/UserRepository'

this.rc.setTo(rcKey, key, value)

You can also pass any value as a second parameter to set multiple properties at once:

const rcKey = 'commands'

this.rc.setTo(rcKey, {
'make:repository': '#app/console/commands/UserRepository'
})

this.rc.pushTo()​

Push a new value to some array property of the RC file:

const rcKey = 'services'

this.rc.pushTo(rcKey, '#app/repositories/MyRepository')

this.rc.save()​

Finally, save all the change made to the RC file. This method needs to always be called at the end:

await this.rc.save()

Executing commands programmatically​

Sometimes you may wish to call other commands from an existing Artisan command or from any other part of your application. You may do so using the call() method from Artisan. This method accepts the command string with its arguments and options:

import { Artisan } from '@athenna/artisan'

await Artisan.call('make:controller TestController')

If you want to verify if your command has generated some output in stdout or stderr you can use the callInChild() method:

const { stdout, stderr } = await Artisan.callInChild('make:controller TestController')

assert.isTrue(stdout.includes('[ MAKING CONTROLLER ]'))
assert.isUndefined(stderr)

By default this method will look to the Path.boostrap('artisan.ts') file to execute your command, but you can change the file to be used setting the path to it as second parameter:

const command = 'make:controller TestController'
const artisanPath = Path.pwd(`artisan.${Path.ext()}`)

await Artisan.callInChild(command, artisanPath)