Creating Custom Commands
Extend Xec's functionality by creating custom commands that integrate seamlessly with the built-in command system.
Overviewβ
Xec supports dynamic command loading from .xec/commands/
directories. Custom commands are JavaScript or TypeScript files that export command definitions compatible with the Commander.js framework used internally by Xec.
Command Structureβ
Custom commands must follow this basic structure:
/**
* Command description (optional)
* This will be available as: xec my-command [args...]
*/
export default function command(program) {
program
.command('my-command [args...]')
.description('A custom command')
.option('-v, --verbose', 'Enable verbose output')
.action(async (args, options) => {
// Your command logic here
});
}
Loading Mechanismβ
Discovery Processβ
Xec discovers commands using the following search pattern:
-
Primary locations (in order):
.xec/commands/
in current directory.xec/cli/
in current directory- Parent directories (up to 3 levels)
-
Environment paths:
- Additional paths from
XEC_COMMANDS_PATH
environment variable (colon-separated)
- Additional paths from
-
File patterns:
.js
,.mjs
,.ts
,.tsx
extensions- Excludes test files (
.test.js
,.spec.ts
, etc.) - Excludes hidden files and type definition files
Command Registrationβ
Commands are loaded in this order:
- Built-in commands (from Xec core)
- Dynamic commands (from directories above)
- Dynamic commands override built-in commands if they share the same name
Command Developmentβ
Basic Command Templateβ
Create a new command file (e.g., .xec/commands/hello.js
):
/**
* A simple hello world command
*/
export default function command(program) {
program
.command('hello [name]')
.description('Say hello to someone')
.option('-u, --uppercase', 'Convert to uppercase')
.option('-c, --count <n>', 'Repeat greeting', '1')
.action(async (name = 'World', options) => {
const { log } = await import('@clack/prompts');
let greeting = `Hello, ${name}!`;
if (options.uppercase) {
greeting = greeting.toUpperCase();
}
const count = parseInt(options.count);
for (let i = 0; i < count; i++) {
log.success(greeting);
}
});
}
Advanced Command with Xec Integrationβ
/**
* Deploy application to multiple targets
*/
export default function command(program) {
program
.command('deploy [targets...]')
.description('Deploy application to specified targets')
.option('-e, --env <environment>', 'Deployment environment', 'production')
.option('--dry-run', 'Show what would be deployed without executing')
.option('-p, --parallel', 'Deploy to all targets in parallel')
.action(async (targets = [], options) => {
const { $, on, copy } = await import('@xec-sh/core');
const { log, spinner } = await import('@clack/prompts');
if (targets.length === 0) {
log.error('No targets specified');
process.exit(1);
}
const s = spinner();
s.start('Preparing deployment...');
try {
// Build application
await $`npm run build`;
if (options.dryRun) {
log.info('Dry run mode - would deploy to:', targets);
return;
}
// Deploy to each target
const deployments = targets.map(async (target) => {
s.message(`Deploying to ${target}...`);
// Copy files
await copy('dist/*', `${target}:/app/`);
// Restart service
await on(target, 'systemctl restart myapp');
return target;
});
if (options.parallel) {
await Promise.all(deployments);
} else {
for (const deployment of deployments) {
await deployment;
}
}
s.stop('Deployment completed successfully');
log.success(`Deployed to ${targets.length} target(s)`);
} catch (error) {
s.stop('Deployment failed');
log.error(error.message);
process.exit(1);
}
});
}
Nested Commandsβ
Create hierarchical command structures using subdirectories:
.xec/commands/
βββ database/
β βββ migrate.js # xec database:migrate
β βββ backup.js # xec database:backup
β βββ restore.js # xec database:restore
βββ cache/
βββ clear.js # xec cache:clear
βββ warm.js # xec cache:warm
Command Metadataβ
Export Metadataβ
Provide rich command information by exporting metadata:
export const metadata = {
description: 'Advanced deployment command',
aliases: ['dep'],
usage: 'deploy <targets...> [options]'
};
export default function command(program) {
// Command implementation
}
JSDoc Commentsβ
Commands can be documented using JSDoc-style comments:
/**
* Command: Deploy application
* Description: Deploy application to multiple targets with rollback support
* Aliases: dep, deploy-app
*/
export default function command(program) {
// Implementation
}
Integration Featuresβ
Xec Core Integrationβ
Access Xec's execution engine and utilities:
export default function command(program) {
program
.command('my-command')
.action(async () => {
// Import Xec core features
const {
$, // Template literal execution
on, // SSH execution
copy, // File copying
forward, // Port forwarding
logs // Log streaming
} = await import('@xec-sh/core');
// Use built-in prompts
const {
log,
spinner,
select,
confirm,
text
} = await import('@clack/prompts');
// Execute commands
const result = await $`echo "Hello from custom command"`;
log.success(result.stdout);
});
}
Configuration Accessβ
Access project configuration in commands:
export default function command(program) {
program
.command('deploy')
.action(async () => {
// Access configuration through global context
const config = global.xecConfig || {};
const targets = config.targets?.hosts || {};
const deployConfig = config.tasks?.deploy || {};
// Use configuration in command logic
});
}
Error Handlingβ
Use Xec's error handling patterns:
export default function command(program) {
program
.command('risky-operation')
.action(async (options) => {
try {
// Command logic that might fail
await performRiskyOperation();
} catch (error) {
const { log } = await import('@clack/prompts');
if (options.verbose) {
log.error('Detailed error:', error.stack);
} else {
log.error(error.message);
}
process.exit(1);
}
});
}
Command Validationβ
File Validationβ
Xec validates command files during loading:
- Must export a default function or named
command
function - Function must call
program.command()
to register at least one command - File must be valid JavaScript/TypeScript
Runtime Validationβ
Commands should validate their inputs:
export default function command(program) {
program
.command('validate-example <required> [optional]')
.action(async (required, optional, options) => {
const { log } = await import('@clack/prompts');
// Validate required parameters
if (!required || required.trim() === '') {
log.error('Required parameter cannot be empty');
process.exit(1);
}
// Validate options
if (options.count && isNaN(parseInt(options.count))) {
log.error('Count must be a number');
process.exit(1);
}
// Command logic
});
}
Best Practicesβ
Command Designβ
- Single Responsibility: Each command should do one thing well
- Consistent Interface: Follow Xec's option and argument patterns
- Error Handling: Provide clear error messages and appropriate exit codes
- Documentation: Include description and usage examples
Performance Considerationsβ
- Lazy Imports: Import heavy modules only when needed
- Async Operations: Use async/await for I/O operations
- Resource Cleanup: Properly close connections and clean up resources
Securityβ
- Input Validation: Validate all user inputs
- Safe Execution: Be careful with shell command construction
- Secrets: Never log sensitive information
Testing Custom Commandsβ
Unit Testingβ
Test command logic separately from CLI integration:
// tests/commands/hello.test.js
import { Command } from 'commander';
import commandSetup from '../../.xec/commands/hello.js';
describe('hello command', () => {
test('registers command correctly', () => {
const program = new Command();
commandSetup(program);
const command = program.commands.find(cmd => cmd.name() === 'hello');
expect(command).toBeDefined();
expect(command.description()).toBe('Say hello to someone');
});
});
Integration Testingβ
Test commands as part of the CLI:
# Test command registration
xec --help | grep "hello"
# Test command execution
xec hello --dry-run
# Test with different options
xec hello Alice --uppercase --count 3
Command Examplesβ
File Management Commandβ
/**
* File management utilities
*/
export default function command(program) {
program
.command('files')
.description('File management utilities')
.addCommand(
new Command('clean')
.description('Clean temporary files')
.option('-f, --force', 'Force deletion without confirmation')
.action(async (options) => {
const { $, glob } = await import('@xec-sh/core');
const { confirm, log } = await import('@clack/prompts');
const files = await glob(['**/*.tmp', '**/*.log']);
if (files.length === 0) {
log.info('No temporary files found');
return;
}
if (!options.force) {
const shouldDelete = await confirm({
message: `Delete ${files.length} temporary files?`
});
if (!shouldDelete) {
log.info('Operation cancelled');
return;
}
}
await $`rm -f ${files}`;
log.success(`Deleted ${files.length} files`);
})
);
}
Environment Commandβ
/**
* Environment management
*/
export default function command(program) {
program
.command('env <action>')
.description('Manage environment configurations')
.option('-e, --environment <name>', 'Environment name')
.action(async (action, options) => {
const { on, copy } = await import('@xec-sh/core');
const { select, log } = await import('@clack/prompts');
let environment = options.environment;
if (!environment) {
environment = await select({
message: 'Select environment:',
options: [
{ value: 'development', label: 'Development' },
{ value: 'staging', label: 'Staging' },
{ value: 'production', label: 'Production' }
]
});
}
switch (action) {
case 'setup':
await setupEnvironment(environment);
break;
case 'deploy':
await deployToEnvironment(environment);
break;
default:
log.error(`Unknown action: ${action}`);
process.exit(1);
}
});
}
async function setupEnvironment(env) {
// Environment setup logic
}
async function deployToEnvironment(env) {
// Deployment logic
}
Troubleshootingβ
Command Not Foundβ
If your command isn't being discovered:
- Check file location (
.xec/commands/
directory) - Verify file extension (
.js
,.ts
, etc.) - Ensure export structure is correct
- Check for syntax errors
- Enable debug mode:
XEC_DEBUG=true xec your-command
Loading Errorsβ
Common loading issues:
# Check command discovery
XEC_DEBUG=true xec --help
# Validate command file
node -c .xec/commands/your-command.js
# Test command registration
node -e "
const { Command } = require('commander');
const cmd = require('./.xec/commands/your-command.js');
const program = new Command();
cmd.default(program);
console.log(program.commands.map(c => c.name()));
"
Performance Issuesβ
If commands load slowly:
- Use dynamic imports for heavy dependencies
- Avoid synchronous I/O in module scope
- Cache expensive computations
- Profile loading time with
XEC_DEBUG=true
Migration and Maintenanceβ
Updating Commandsβ
When updating Xec or dependencies:
- Test command compatibility
- Update import statements if needed
- Check for deprecated APIs
- Update documentation
Sharing Commandsβ
To share commands across projects:
- Create a shared commands repository
- Use
XEC_COMMANDS_PATH
environment variable - Consider publishing as npm packages
- Document dependencies and requirements
Commands provide a powerful way to extend Xec's functionality while maintaining consistency with the built-in command system. Follow these patterns and best practices to create robust, maintainable custom commands that integrate seamlessly with your Xec workflows.