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 src/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 src/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": "#src/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": "#src/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 '#src/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}`)
}
}
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": "#src/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:
{
"commands": {
"sendEmails": {
"path": "#src/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 '#src/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 '#src/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 '#src/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)
}
}
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 '#src/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 = 'src/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)