Command Hooks ​
Gunshi provides powerful lifecycle hooks that allow you to intercept and control command execution at various stages.
These hooks enable advanced scenarios like logging, monitoring, validation, and error handling.
Understanding Command Lifecycle ​
The command execution lifecycle in Gunshi follows these stages:
Available Hooks ​
Gunshi provides three main lifecycle hooks:
onBeforeCommand: Executes before the command runsonAfterCommand: Executes after successful command completiononErrorCommand: Executes when a command throws an error
Basic Hook Usage ​
Setting Up Hooks ​
The following example demonstrates how to configure lifecycle hooks when initializing your CLI application.
In this setup, we define three hooks that will execute at different stages of the command lifecycle:
import { cli } from 'gunshi'
await cli(
process.argv.slice(2),
{
name: 'server',
run: () => {
console.log('Starting server...')
}
},
{
name: 'my-app',
version: '1.0.0',
// Define lifecycle hooks
onBeforeCommand: ctx => {
console.log(`About to run: ${ctx.name}`)
},
onAfterCommand: (ctx, result) => {
console.log(`Command ${ctx.name} completed successfully`)
},
onErrorCommand: (ctx, error) => {
console.error(`Command ${ctx.name} failed:`, error)
}
}
)TIP
The example fully code is here.
Hook Parameters ​
Each lifecycle hook receives specific parameters that provide context about the command execution.
The CommandContext parameter is read-only and contains all command metadata, while onAfterCommand also receives the command result and onErrorCommand receives the thrown error:
{
// Before command execution
onBeforeCommand?: (ctx: Readonly<CommandContext>) => Awaitable<void>
// After successful execution
onAfterCommand?: (ctx: Readonly<CommandContext>, result: string | undefined) => Awaitable<void>
// On command error
onErrorCommand?: (ctx: Readonly<CommandContext>, error: Error) => Awaitable<void>
}NOTE
The CommandContext object provides comprehensive information about command execution. For the complete CommandContext API reference including all properties and types, see the CommandContext interface documentation.
With an understanding of how hooks work and their parameters, let's explore how they differ from and interact with plugin decorators.
Hooks vs Decorators ​
Gunshi provides two distinct mechanisms for controlling command execution:
CLI-level Hooks: Lifecycle hooks that run before and after command execution
onBeforeCommand: Pre-execution processing (logging, validation, initialization)onAfterCommand: Post-success processing (cleanup, metrics recording)onErrorCommand: Error handling (error logging, rollback)
Plugin Decorators: Wrap the command itself to modify its behavior
decorateCommand: Wraps command runner to add or modify functionality- Multiple plugins can chain decorators (decorator pattern)
TIP
The decorateCommand method is a powerful plugin API that allows wrapping command execution to add or modify functionality. It enables plugins to implement cross-cutting concerns like authentication, logging, and transaction management. For comprehensive information about how to use decorators in plugins, see the Plugin Decorators documentation.
Execution Flow ​
The following diagram illustrates how hooks and decorators interact during command execution:
Detailed Execution Sequence ​
onBeforeCommandHook - Pre-execution setup and validation- Plugin Decorator Chain - Command wrapping by plugins
- Applied in reverse order (LIFO - last registered, first executed)
- Each decorator wraps the next runner in the chain
- Command Runner - Actual command execution
onAfterCommandHook - Post-success processingonErrorCommandHook - Error handling when exceptions occur
Plugin Decorator Example ​
The following example demonstrates how to use the decorateCommand method in a plugin to measure command execution time:
import { plugin } from 'gunshi/plugin'
// Using decorateCommand in a plugin
export default plugin({
id: 'timing-plugin',
setup: ctx => {
// Wrap command execution to measure execution time
ctx.decorateCommand(baseRunner => {
return async commandCtx => {
const start = Date.now()
try {
const result = await baseRunner(commandCtx)
console.log(`Execution time: ${Date.now() - start}ms`)
return result
} catch (error) {
console.log(`Failed after: ${Date.now() - start}ms`)
throw error
}
}
})
}
})NOTE
Plugins don't have CLI-level hooks (onBeforeCommand, etc.). Instead, they use the decorateCommand method to wrap command execution and add custom logic. This allows plugins to extend and modify command behavior through the decorator pattern.
Practical Use Cases ​
TIP
The following examples use plugin extensions through ctx.extensions. Extensions are how plugins add functionality to the command context, allowing you to access plugin-provided features like logging, metrics, authentication, and database connections. For comprehensive information about working with extensions, including type-safe patterns and best practices, see the Context Extensions documentation.
Logging and Monitoring ​
Implement comprehensive logging across all commands:
import { cli } from 'gunshi'
import logger, { pluginId as loggerId } from '@my/plugin-logger'
await cli(process.argv.slice(2), commands, {
name: 'my-app',
// Install logger plugin
plugins: [logger()],
onBeforeCommand: ctx => {
const logger = ctx.extensions[loggerId]
// Log command start with arguments
logger?.info('Command started', {
command: ctx.name,
args: ctx.values,
timestamp: new Date().toISOString()
})
},
onAfterCommand: (ctx, result) => {
const logger = ctx.extensions[loggerId]
// Log successful completion
logger?.info('Command completed', {
command: ctx.name,
duration: Date.now() - logger?.startTime,
result: typeof result
})
},
onErrorCommand: (ctx, error) => {
const logger = ctx.extensions[loggerId]
// Log errors with full context
logger?.error('Command failed', {
command: ctx.name,
error: error.message,
stack: error.stack,
args: ctx.values
})
}
})Performance Monitoring ​
Track command execution times and performance metrics:
import { cli } from 'gunshi'
import metrics, { pluginId as metricsId } from '@my/plugin-metrics'
await cli(process.argv.slice(2), commands, {
name: 'my-app',
// Install metrics plugin
plugins: [metrics()],
onBeforeCommand: ctx => {
const metrics = ctx.extensions[metricsId]
// Start tracking command execution
metrics?.startTracking({
command: ctx.name,
args: ctx.values,
environment: process.env.NODE_ENV
})
},
onAfterCommand: async (ctx, result) => {
const metrics = ctx.extensions[metricsId]
// Record successful completion
const duration = metrics?.endTracking()
// Send metrics to monitoring service
await metrics?.send({
command: ctx.name,
status: 'success',
duration,
memoryUsage: process.memoryUsage(),
resultSize: result?.length || 0
})
},
onErrorCommand: async (ctx, error) => {
const metrics = ctx.extensions[metricsId]
// Record failure metrics
const duration = metrics?.endTracking()
// Send error metrics with additional context
await metrics?.send({
command: ctx.name,
status: 'failed',
duration,
error: error.message,
errorType: error.constructor.name,
stackTrace: error.stack
})
}
})Validation and Guards ​
Use hooks to implement global validation or access control:
import { cli } from 'gunshi'
import auth, { pluginId as authId } from '@my/plugin-auth'
await cli(process.argv.slice(2), commands, {
name: 'my-app',
// Install auth plugin
plugins: [
auth({
publicCommands: ['help', 'version', 'login'],
tokenSource: ['env:AUTH_TOKEN', 'args:token']
})
],
onBeforeCommand: async ctx => {
const auth = ctx.extensions[authId]
// Skip auth for public commands
if (auth?.isPublicCommand(ctx.name)) {
return
}
// Verify authentication
const token = auth?.getToken()
if (!token) {
throw new Error('Authentication required. Please run "login" first.')
}
const user = await auth?.verifyToken(token)
if (!user) {
throw new Error('Invalid or expired token. Please login again.')
}
// Store user info for command use
await auth?.setCurrentUser(user)
},
onAfterCommand: async ctx => {
const auth = ctx.extensions[authId]
// Clean up sensitive data after command execution
await auth?.clearSession()
},
onErrorCommand: async (ctx, error) => {
const auth = ctx.extensions[authId]
// Log security-related errors
if (error.message.includes('Authentication') || error.message.includes('token')) {
await auth?.logSecurityEvent({
type: 'auth_failure',
command: ctx.name,
timestamp: new Date().toISOString()
})
}
}
})Transaction Management ​
Implement database transactions or rollback mechanisms:
import { cli } from 'gunshi'
import database, { pluginId as dbId } from '@my/plugin-database'
await cli(process.argv.slice(2), commands, {
name: 'my-app',
// Install database plugin with transaction support
plugins: [
database({
connectionString: process.env.DATABASE_URL,
transactionalCommands: ['create', 'update', 'delete', 'migrate']
})
],
onBeforeCommand: async ctx => {
const db = ctx.extensions[dbId]
// Start transaction for data-modifying commands
if (db?.isTransactionalCommand(ctx.name)) {
const transaction = await db.beginTransaction()
// Store transaction ID for tracking
await db?.setCurrentTransaction(transaction.id)
console.log(`Transaction ${transaction.id} started for command: ${ctx.name}`)
}
},
onAfterCommand: async (ctx, result) => {
const db = ctx.extensions[dbId]
const transaction = db?.getCurrentTransaction()
if (transaction) {
// Commit on success
await db.commit(transaction.id)
console.log(`Transaction ${transaction.id} committed successfully`)
// Clean up transaction reference
await db.clearCurrentTransaction()
}
},
onErrorCommand: async (ctx, error) => {
const db = ctx.extensions[dbId]
const transaction = db?.getCurrentTransaction()
if (transaction) {
// Rollback on error
await db?.rollback(transaction.id)
console.error(`Transaction ${transaction.id} rolled back due to error:`, error.message)
// Log the failed transaction for audit
await db?.logTransactionFailure({
id: transaction.id,
command: ctx.name,
error: error.message,
timestamp: new Date().toISOString()
})
// Clean up transaction reference
await db?.clearCurrentTransaction()
}
}
})Hook Execution Order ​
When multiple hooks and decorators are present, they execute in a specific sequence as shown in the Hooks vs Decorators section above.
Understanding this order is crucial for implementing complex behaviors like transaction management or error recovery.
For detailed execution flow, refer to the execution diagram in the Hooks vs Decorators section.
