Middleware
Intercept and transform command execution with middleware.
Middleware lets you run code before and after command execution. It follows the classic "onion model" pattern — each middleware wraps the next, forming a chain.
Defining Middleware
A middleware is a function that receives the seed context and a next function:
import type { Middleware } from '@seedcli/core'
const timing: Middleware = async (seed, next) => {
const start = Date.now()
await next() // Run the next middleware or command
const elapsed = Date.now() - start
seed.print.muted(`Completed in ${elapsed}ms`)
}Global Middleware
Global middleware runs for every command:
build('my-cli')
.middleware(async (seed, next) => {
seed.print.debug(`Running: ${seed.meta.commandName}`)
await next()
})
.middleware(async (seed, next) => {
try {
await next()
} catch (error) {
seed.print.error(`Command failed: ${error.message}`)
process.exit(1)
}
})
.create()Multiple middleware are executed in the order they're registered.
Command-Level Middleware
Middleware can be scoped to individual commands:
const requireAuth: Middleware = async (seed, next) => {
if (!seed.auth?.isAuthenticated) {
seed.print.error('Authentication required. Run "my-cli login" first.')
return
}
await next()
}
command({
name: 'deploy',
middleware: [requireAuth],
run: async (seed) => {
// Only runs if authenticated
},
})Execution Order
Middleware forms a nested chain:
Global middleware 1
→ Global middleware 2
→ Command middleware 1
→ Command middleware 2
→ command.run(seed)
← Command middleware 2
← Command middleware 1
← Global middleware 2
← Global middleware 1Common Patterns
Error Handling
const errorHandler: Middleware = async (seed, next) => {
try {
await next()
} catch (error) {
if (seed.meta.debug) {
console.error(error)
} else {
seed.print.error(error.message)
}
process.exit(1)
}
}Logging
const logger: Middleware = async (seed, next) => {
seed.print.debug(`→ ${seed.meta.commandName}`)
seed.print.debug(` args: ${JSON.stringify(seed.args)}`)
seed.print.debug(` flags: ${JSON.stringify(seed.flags)}`)
await next()
seed.print.debug(`← ${seed.meta.commandName}`)
}Confirmation
const confirmDestructive: Middleware = async (seed, next) => {
if (!seed.flags.force) {
const ok = await seed.prompt.confirm({ message: 'This is a destructive action. Continue?' })
if (!ok) {
seed.print.info('Aborted.')
return
}
}
await next()
}Analytics
const analytics: Middleware = async (seed, next) => {
const start = Date.now()
let status = 'success'
try {
await next()
} catch (error) {
status = 'error'
throw error
} finally {
await trackEvent({
command: seed.meta.commandName,
duration: Date.now() - start,
status,
})
}
}Short-Circuiting
If you don't call next(), the remaining middleware and command won't execute:
const versionCheck: Middleware = async (seed, next) => {
const required = '>=1.0.0'
const current = seed.meta.version
if (!seed.semver.satisfies(current, required)) {
seed.print.error(`Version ${current} not supported. Requires ${required}.`)
return // Don't call next() — command won't run
}
await next()
}