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)
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 = '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()
β
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:
export class {{ namePascal }} {
}
Now we map our new template in .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:
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
}
}
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}`
}
}
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 = '#src/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': '#src/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, '#src/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.bin('artisan.ts')
./bin/artisan.ts
const command = 'make:controller TestController'
const artisanPath = Path.pwd(`artisan.${Path.ext()}`)
await Artisan.callInChild(command, artisanPath)