@xec-sh/cli
Version:
Xec: The Universal Shell for TypeScript
1,809 lines (1,585 loc) • 55.6 kB
JavaScript
import path from 'path';
import fs from 'fs-extra';
import chalk from 'chalk';
import * as clack from '@clack/prompts';
import { InteractiveHelpers } from '../utils/interactive-helpers.js';
import { BaseCommand } from '../utils/command-base.js';
import { ConfigurationManager } from '../config/configuration-manager.js';
const TEMPLATES = {
project: {
minimal: {
files: {
'.xec/config.yaml': `version: "1.0"
name: {name}
description: {description}
# Define your targets
targets:
hosts:
# example:
# host: example.com
# user: deploy
# Define your tasks
tasks:
hello:
description: Example task
command: echo "Hello from Xec!"
`,
'.xec/.gitignore': `.env
.env.*
cache/
logs/
tmp/
`,
}
},
standard: {
files: {
'.xec/config.yaml': `version: "1.0"
name: {name}
description: {description}
# Variables for reuse
vars:
app_name: {name}
deploy_path: /opt/apps/{name}
log_level: info
# Define your targets
targets:
# Default settings for all targets
defaults:
timeout: 30s
shell: /bin/bash
# SSH hosts
hosts:
# staging:
# host: staging.example.com
# user: deploy
# privateKey: ~/.ssh/id_rsa
# production:
# host: prod.example.com
# user: deploy
# privateKey: ~/.ssh/id_rsa
# Docker containers
containers:
# app:
# image: node:18
# volumes:
# - ./:/app
# workdir: /app
# Kubernetes pods
pods:
# web:
# namespace: default
# selector: app=web
# Environment profiles
profiles:
development:
vars:
log_level: debug
env:
NODE_ENV: development
production:
vars:
log_level: error
env:
NODE_ENV: production
# Executable tasks
tasks:
# Simple command task
hello:
description: Say hello
command: echo "Hello from {name}!"
# Multi-step task
deploy:
description: Deploy application
params:
- name: version
required: true
description: Version to deploy
steps:
- name: Build application
command: npm run build
- name: Run tests
command: npm test
onFailure: abort
- name: Deploy to servers
targets: [hosts.staging, hosts.production]
command: |
cd \${vars.deploy_path}
git pull origin \${params.version}
npm install --production
npm run migrate
pm2 reload app
# Scheduled backup task
backup:
description: Backup database and files
schedule: "0 2 * * *" # 2 AM daily
target: hosts.production
command: |
pg_dump myapp > /backup/db-$(date +%Y%m%d).sql
tar -czf /backup/files-$(date +%Y%m%d).tar.gz /app/uploads
# Script configuration
scripts:
env:
API_URL: https://api.example.com
globals:
- lodash
- dayjs
# Command defaults
commands:
copy:
compress: true
progress: true
watch:
interval: 1000
clear: true
# Secrets configuration
secrets:
provider: local
config:
storageDir: .xec/secrets
`,
'.xec/.gitignore': `.env
.env.*
cache/
logs/
tmp/
secrets/
*.log
`,
'.xec/scripts/example.js': `#!/usr/bin/env xec
/**
* Example Xec script
* Run with: xec scripts/example.js
*/
// Use the $ function from @xec-sh/core
const result = await $\`echo "Hello from Xec!"\`;
log.success(result.stdout);
// Interactive prompts
const name = await question({
message: 'What is your name?',
defaultValue: 'World'
});
log.info(\`Hello, \${name}!\`);
// Work with files
const files = await glob('*.js');
log.step(\`Found \${files.length} JavaScript files\`);
// HTTP requests
const response = await fetch('https://api.github.com/users/github');
const data = await response.json();
log.info(\`GitHub has \${data.public_repos} public repos\`);
// Parallel execution
const results = await Promise.all([
$\`echo "Task 1"\`,
$\`echo "Task 2"\`,
$\`echo "Task 3"\`
]);
log.success('All tasks completed!');
`,
'.xec/commands/hello.js': `/**
* Example dynamic CLI command
* This will be available as: xec hello [name]
*/
export function command(program) {
program
.command('hello [name]')
.description('Say hello')
.option('-u, --uppercase', 'Output in uppercase')
.option('-r, --repeat <times>', 'Repeat the message', '1')
.action(async (name = 'World', options) => {
const { log } = await import('@clack/prompts');
let message = \`Hello, \${name}!\`;
if (options.uppercase) {
message = message.toUpperCase();
}
const times = parseInt(options.repeat, 10);
for (let i = 0; i < times; i++) {
log.success(message);
}
});
}
`,
'.xec/README.md': `# {name}
{description}
## Getting Started
1. Run example script:
\`\`\`bash
xec scripts/example.js
\`\`\`
2. Try the custom command:
\`\`\`bash
xec hello YourName
\`\`\`
3. Run a task:
\`\`\`bash
xec tasks:run hello
\`\`\`
## Project Structure
- \`.xec/config.yaml\` - Project configuration
- \`.xec/scripts/\` - Xec scripts
- \`.xec/commands/\` - Custom CLI commands
- \`.xec/cache/\` - Cache directory
- \`.xec/logs/\` - Log files
## Configuration
Edit \`.xec/config.yaml\` to:
- Add SSH hosts, Docker containers, or Kubernetes pods
- Define reusable tasks
- Set up environment profiles
- Configure secrets management
## Learn More
- [Xec Documentation](https://xec.sh/docs)
- [Configuration Reference](https://xec.sh/docs/config)
- [Task System](https://xec.sh/docs/tasks)
`,
}
}
},
script: {
basic: {
js: `#!/usr/bin/env xec
/**
* {description}
*
* Usage: xec {filepath}
*/
// Your script logic here
log.info('Running {name} script...');
// Example: Run a command
const result = await $\`echo "Hello from {name} script!"\`;
log.success(result.stdout);
// Example: Work with files
const files = await glob('**/*.js');
log.step(\`Found \${files.length} JavaScript files\`);
// Example: Interactive prompt
const answer = await question({
message: 'Continue with the operation?',
defaultValue: 'yes'
});
if (answer.toLowerCase() === 'yes') {
log.success('Operation completed successfully!');
} else {
log.info('Operation cancelled');
}
`,
ts: `#!/usr/bin/env xec
/**
* {description}
*
* Usage: xec {filepath}
*/
// Type-safe command execution
const result = await $\`echo "Hello from TypeScript!"\`;
log.success(result.stdout);
// Work with files using built-in fs
const files = await glob('**/*.ts');
log.step(\`Found \${files.length} TypeScript files\`);
// Interactive prompts with type inference
const name = await question({
message: 'What is your name?',
defaultValue: 'Developer'
});
// Use chalk for colored output
log.info(chalk.blue(\`Hello, \${name}!\`));
// Example: Fetch data from API
interface GitHubRepo {
name: string;
description: string;
stargazers_count: number;
}
try {
const response = await fetch('https://api.github.com/repos/xec-sh/xec');
const repo: GitHubRepo = await response.json();
log.step(\`Repo: \${chalk.cyan(repo.name)}\`);
log.step(\`Stars: \${chalk.yellow('⭐')} \${repo.stargazers_count}\`);
} catch (error) {
log.error(\`Failed to fetch repo info: \${error}\`);
}
log.success('✅ Script completed successfully!');
`
},
advanced: {
js: `#!/usr/bin/env xec
/**
* {description}
*
* Usage: xec {filepath} [options]
*
* Options:
* --env <environment> Environment to run in
* --dry-run Show what would be done
* --verbose Enable verbose output
*/
// Parse command line arguments
const options = {
env: argv.env || 'development',
dryRun: argv['dry-run'] || false,
verbose: argv.verbose || false
};
// Configure logging
if (options.verbose) {
$.verbose = true;
}
// Load configuration
const config = await xec.config.load();
const profile = config.profiles?.[options.env];
if (profile) {
xec.config.applyProfile(options.env);
log.info(chalk.dim(\`Applied profile: \${options.env}\`));
}
log.info(chalk.bold('🚀 {description}'));
log.step(\`Environment: \${options.env}\`);
log.step(\`Dry run: \${options.dryRun}\`);
const spinner = clack.spinner();
try {
// Step 1: Validation
spinner.start('Validating environment...');
await validateEnvironment(options.env);
spinner.stop('Environment validated');
// Step 2: Main operation
if (!options.dryRun) {
spinner.start('Executing main operation...');
await executeMainOperation(options);
spinner.stop('Operation completed');
} else {
log.info('Dry run mode - skipping execution');
}
// Step 3: Cleanup
spinner.start('Cleaning up...');
await cleanup();
spinner.stop('Cleanup completed');
log.success(chalk.green('✅ Script completed successfully!'));
} catch (error) {
spinner.stop(chalk.red('Operation failed'));
log.error(error.message);
process.exit(1);
}
// Helper functions
async function validateEnvironment(env) {
const validEnvs = ['development', 'staging', 'production'];
if (!validEnvs.includes(env)) {
throw new Error(\`Invalid environment: \${env}\`);
}
}
async function executeMainOperation(options) {
// Example: Use targets from config
const targets = await xec.config.getTargets();
if (options.env === 'production' && targets.hosts?.production) {
const $prod = $.ssh(targets.hosts.production);
await $prod\`uptime\`;
await $prod\`df -h\`;
} else {
// Local operations
await $\`echo "Running in \${options.env} mode"\`;
}
}
async function cleanup() {
if (await fs.exists('.tmp')) {
await fs.remove('.tmp');
}
}
`,
ts: `#!/usr/bin/env xec
/**
* {description}
*
* Usage: xec {filepath} [options]
*
* Options:
* --env <environment> Environment to run in
* --dry-run Show what would be done
* --verbose Enable verbose output
*/
import type { ExecutionEngine } from '@xec-sh/core';
// Parse command line arguments with type safety
interface Options {
env: 'development' | 'staging' | 'production';
dryRun: boolean;
verbose: boolean;
_: string[];
}
const options = argv as unknown as Options;
const environment = options.env || 'development';
const isDryRun = options['dry-run'] || false;
const isVerbose = options.verbose || false;
// Configure execution
if (isVerbose) {
$.verbose = true;
}
// Load and apply configuration
const config = await xec.config.load();
const profile = config.profiles?.[environment];
if (profile) {
xec.config.applyProfile(environment);
log.info(chalk.dim(\`Applied profile: \${environment}\`));
}
log.info(chalk.bold('🚀 {description}'));
log.step(\`Environment: \${chalk.cyan(environment)}\`);
log.step(\`Dry run: \${isDryRun ? chalk.yellow('yes') : chalk.green('no')}\`);
// Type-safe configuration
interface Config {
apiUrl: string;
timeout: number;
retries: number;
}
const configs: Record<typeof environment, Config> = {
development: {
apiUrl: 'http://localhost:3000',
timeout: 5000,
retries: 1
},
staging: {
apiUrl: 'https://staging.example.com',
timeout: 10000,
retries: 2
},
production: {
apiUrl: 'https://api.example.com',
timeout: 30000,
retries: 3
}
};
const envConfig = configs[environment];
const spinner = clack.spinner();
try {
// Step 1: Validation
spinner.start('Validating environment...');
await validateEnvironment(environment);
spinner.stop('Environment validated');
// Step 2: Connect to services
if (environment === 'production') {
spinner.start('Connecting to production services...');
const targets = await xec.config.getTargets();
const prodHost = targets.hosts?.production;
if (prodHost && !isDryRun) {
const $prod = $.ssh(prodHost);
const uptimeResult = await $prod\`uptime\`;
log.step('Server uptime: ' + uptimeResult.stdout.trim());
}
spinner.stop('Connected to production');
}
// Step 3: Main operation with retry logic
if (!isDryRun) {
spinner.start('Executing main operation...');
await retry(
async () => {
await executeMainOperation(envConfig);
},
{
retries: envConfig.retries,
delay: 1000,
onRetry: (error, attempt) => {
log.warning(\`Retry \${attempt}/\${envConfig.retries}: \${error.message}\`);
}
}
);
spinner.stop('Operation completed');
} else {
log.info('Dry run mode - skipping execution');
}
// Step 4: Cleanup
spinner.start('Cleaning up...');
await cleanup();
spinner.stop('Cleanup completed');
log.success(chalk.green('✅ Script completed successfully!'));
} catch (error) {
spinner.stop(chalk.red('Operation failed'));
log.error(error instanceof Error ? error.message : String(error));
process.exit(1);
}
// Helper functions with proper typing
async function validateEnvironment(env: string): Promise<void> {
const validEnvs = ['development', 'staging', 'production'];
if (!validEnvs.includes(env)) {
throw new Error(\`Invalid environment: \${env}\`);
}
// Check required environment variables
const requiredVars = ['API_KEY'];
const missing = requiredVars.filter(v => !process.env[v]);
if (missing.length > 0) {
throw new Error(\`Missing required environment variables: \${missing.join(', ')}\`);
}
}
async function executeMainOperation(config: Config): Promise<void> {
// Example: API call with timeout
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), config.timeout);
try {
const response = await fetch(config.apiUrl + '/health', {
signal: controller.signal,
headers: {
'Authorization': \`Bearer \${env('API_KEY')}\`
}
});
if (!response.ok) {
throw new Error(\`API returned \${response.status}\`);
}
const data = await response.json();
log.step(\`API health: \${data.status}\`);
} finally {
clearTimeout(timeout);
}
}
async function cleanup(): Promise<void> {
const tempFiles = await glob('.tmp-{name}-*');
for (const file of tempFiles) {
await fs.remove(file);
}
}
`
}
},
command: {
basic: {
js: `/**
* {description}
*
* This command will be available as: xec {name} [arguments]
*/
export function command(program) {
program
.command('{name} [args...]')
.description('{description}')
.option('-v, --verbose', 'Enable verbose output')
.action(async (args, options) => {
const { log } = await import('@clack/prompts');
// Your command logic here
log.info('Running {name} command...');
if (options.verbose) {
log.step('Verbose mode enabled');
log.step(\`Arguments: \${args.join(', ') || 'none'}\`);
}
// Example: Use $ from @xec-sh/core
const { $ } = await import('@xec-sh/core');
const result = await $\`echo "Command {name} executed successfully!"\`;
log.success(result.stdout);
});
}
`,
ts: `/**
* {description}
*
* This command will be available as: xec {name} [arguments]
*/
import type { Command } from 'commander';
export function command(program: Command): void {
program
.command('{name} [args...]')
.description('{description}')
.option('-v, --verbose', 'Enable verbose output')
.option('-f, --format <type>', 'Output format', 'json')
.action(async (args: string[], options: { verbose: boolean; format: string }) => {
const { log } = await import('@clack/prompts');
// Your command logic here
log.info('Running {name} command...');
if (options.verbose) {
log.step('Verbose mode enabled');
log.step(\`Arguments: \${args.join(', ') || 'none'}\`);
log.step(\`Format: \${options.format}\`);
}
// Example: Use $ from @xec-sh/core with type safety
const { $ } = await import('@xec-sh/core');
const result = await $\`echo "Command {name} executed successfully!"\`;
// Format output based on option
if (options.format === 'json') {
console.log(JSON.stringify({
success: true,
message: result.stdout.trim(),
args
}, null, 2));
} else {
log.success(result.stdout);
}
});
}
`
},
advanced: {
js: `/**
* {description}
*
* This command will be available as: xec {name} <action> [options]
*
* Examples:
* xec {name} list
* xec {name} create myitem --type=example
* xec {name} delete myitem --force
*/
export function command(program) {
const cmd = program
.command('{name}')
.description('{description}');
// Subcommand: list
cmd
.command('list')
.description('List all items')
.option('-f, --filter <pattern>', 'Filter results')
.action(async (options) => {
const { log, spinner } = await import('@clack/prompts');
const { $ } = await import('@xec-sh/core');
const s = spinner();
s.start('Loading items...');
try {
// Your list logic here
const items = await getItems(options.filter);
s.stop('Found ' + items.length + ' items');
if (items.length === 0) {
log.info('No items found');
return;
}
// Display items
items.forEach(item => {
log.step(\`• \${item.name} (\${item.type})\`);
});
} catch (error) {
s.stop('Failed to load items');
log.error(error.message);
process.exit(1);
}
});
// Subcommand: create
cmd
.command('create <name>')
.description('Create a new item')
.option('-t, --type <type>', 'Item type', 'default')
.option('-d, --description <desc>', 'Item description')
.action(async (name, options) => {
const { log, confirm } = await import('@clack/prompts');
const { $ } = await import('@xec-sh/core');
log.info(\`Creating item: \${name}\`);
log.step(\`Type: \${options.type}\`);
if (options.description) {
log.step(\`Description: \${options.description}\`);
}
const shouldCreate = await confirm({
message: 'Proceed with creation?',
initialValue: true
});
if (!shouldCreate) {
log.info('Creation cancelled');
return;
}
try {
// Your create logic here
await createItem(name, options);
log.success(\`Item '\${name}' created successfully!\`);
} catch (error) {
log.error(\`Failed to create item: \${error.message}\`);
process.exit(1);
}
});
// Subcommand: delete
cmd
.command('delete <name>')
.description('Delete an item')
.option('-f, --force', 'Skip confirmation')
.action(async (name, options) => {
const { log, confirm } = await import('@clack/prompts');
if (!options.force) {
const shouldDelete = await confirm({
message: \`Are you sure you want to delete '\${name}'?\`,
initialValue: false
});
if (!shouldDelete) {
log.info('Deletion cancelled');
return;
}
}
try {
// Your delete logic here
await deleteItem(name);
log.success(\`Item '\${name}' deleted successfully!\`);
} catch (error) {
log.error(\`Failed to delete item: \${error.message}\`);
process.exit(1);
}
});
}
// Helper functions
async function getItems(filter) {
// Mock implementation - replace with your logic
const allItems = [
{ name: 'example1', type: 'script' },
{ name: 'example2', type: 'command' }
];
if (filter) {
return allItems.filter(item =>
item.name.includes(filter) || item.type.includes(filter)
);
}
return allItems;
}
async function createItem(name, options) {
const { $ } = await import('@xec-sh/core');
await $\`echo "Creating item: \${name} (type: \${options.type})"\`;
}
async function deleteItem(name) {
const { $ } = await import('@xec-sh/core');
await $\`echo "Deleting item: \${name}"\`;
}
`,
ts: `/**
* {description}
*
* This command will be available as: xec {name} <action> [options]
*
* Examples:
* xec {name} list --filter=active
* xec {name} create myitem --type=service
* xec {name} delete myitem --force
*/
import type { Command } from 'commander';
// Type definitions
interface Item {
id: string;
name: string;
type: 'service' | 'task' | 'resource';
status: 'active' | 'inactive' | 'pending';
created: Date;
metadata?: Record<string, any>;
}
interface ListOptions {
filter?: string;
type?: Item['type'];
status?: Item['status'];
json?: boolean;
}
interface CreateOptions {
type: Item['type'];
description?: string;
}
export function command(program: Command): void {
const cmd = program
.command('{name}')
.description('{description}');
// Subcommand: list
cmd
.command('list')
.description('List all items')
.option('-f, --filter <pattern>', 'Filter by name pattern')
.option('-t, --type <type>', 'Filter by type')
.option('-s, --status <status>', 'Filter by status')
.option('--json', 'Output as JSON')
.action(async (options: ListOptions) => {
const { log, spinner } = await import('@clack/prompts');
const { $ } = await import('@xec-sh/core');
const s = spinner();
s.start('Loading items...');
try {
const items = await getItems(options);
s.stop('Found ' + items.length + ' items');
if (items.length === 0) {
log.info('No items found');
return;
}
if (options.json) {
console.log(JSON.stringify(items, null, 2));
} else {
const chalk = await import('chalk');
console.log(chalk.default.bold('\\nItems:'));
console.log(chalk.default.gray('─'.repeat(60)));
items.forEach(item => {
const statusColor = item.status === 'active' ? 'green' :
item.status === 'inactive' ? 'red' : 'yellow';
const statusText = statusColor === 'green' ? chalk.default.green(item.status) :
statusColor === 'red' ? chalk.default.red(item.status) :
chalk.default.yellow(item.status);
console.log(
chalk.default.bold(item.name) + ' ' +
'(' + chalk.default.blue(item.type) + ') - ' +
statusText
);
});
console.log(chalk.default.gray('─'.repeat(60)));
}
} catch (error) {
s.stop('Failed to load items');
log.error(error instanceof Error ? error.message : String(error));
process.exit(1);
}
});
// Subcommand: create
cmd
.command('create <name>')
.description('Create a new item')
.requiredOption('-t, --type <type>', 'Item type (service|task|resource)')
.option('-d, --description <desc>', 'Item description')
.action(async (name: string, options: CreateOptions) => {
const { log, confirm } = await import('@clack/prompts');
const { $ } = await import('@xec-sh/core');
const chalk = await import('chalk');
// Validate options
const validTypes: Item['type'][] = ['service', 'task', 'resource'];
if (!validTypes.includes(options.type)) {
log.error(\`Invalid type: \${options.type}. Must be one of: \${validTypes.join(', ')}\`);
process.exit(1);
}
// Display creation summary
log.info(chalk.default.bold('Creating new item:'));
log.step(\`Name: \${chalk.default.cyan(name)}\`);
log.step(\`Type: \${chalk.default.blue(options.type)}\`);
if (options.description) {
log.step(\`Description: \${options.description}\`);
}
const shouldCreate = await confirm({
message: 'Proceed with creation?',
initialValue: true
});
if (!shouldCreate) {
log.info('Creation cancelled');
return;
}
try {
const item: Item = {
id: crypto.randomUUID(),
name,
type: options.type,
status: 'pending',
created: new Date(),
metadata: { description: options.description }
};
await createItem(item);
log.success(\`Item '\${name}' created successfully!\\nID: \${item.id}\`);
} catch (error) {
log.error(\`Failed to create item: \${error instanceof Error ? error.message : String(error)}\`);
process.exit(1);
}
});
// Subcommand: delete
cmd
.command('delete <name>')
.description('Delete an item')
.option('-f, --force', 'Skip confirmation')
.action(async (name: string, options: { force?: boolean }) => {
const { log, confirm } = await import('@clack/prompts');
const chalk = await import('chalk');
// Check if item exists
const item = await getItemByName(name);
if (!item) {
log.error(\`Item '\${name}' not found\`);
process.exit(1);
}
// Show item details
log.info(chalk.default.bold('Item to delete:'));
log.step(\`Name: \${item.name}\`);
log.step(\`Type: \${item.type}\`);
log.step(\`Status: \${item.status}\`);
if (!options.force) {
const shouldDelete = await confirm({
message: chalk.default.red(\`Are you sure you want to delete '\${name}'?\`),
initialValue: false
});
if (!shouldDelete) {
log.info('Deletion cancelled');
return;
}
}
try {
await deleteItem(item.id);
log.success(\`Item '\${name}' deleted successfully!\`);
} catch (error) {
log.error(\`Failed to delete item: \${error instanceof Error ? error.message : String(error)}\`);
process.exit(1);
}
});
}
// Helper functions
async function getItems(options: ListOptions): Promise<Item[]> {
// Mock implementation - replace with your logic
const allItems: Item[] = [
{
id: '1',
name: 'example-service',
type: 'service',
status: 'active',
created: new Date('2024-01-01')
},
{
id: '2',
name: 'backup-task',
type: 'task',
status: 'pending',
created: new Date('2024-01-02')
}
];
let filtered = allItems;
if (options.filter) {
const pattern = new RegExp(options.filter, 'i');
filtered = filtered.filter(item => pattern.test(item.name));
}
if (options.type) {
filtered = filtered.filter(item => item.type === options.type);
}
if (options.status) {
filtered = filtered.filter(item => item.status === options.status);
}
return filtered;
}
async function getItemByName(name: string): Promise<Item | null> {
const items = await getItems({});
return items.find(item => item.name === name) || null;
}
async function createItem(item: Item): Promise<void> {
const { $ } = await import('@xec-sh/core');
await $\`echo "Creating item: \${item.name} (ID: \${item.id})"\`;
}
async function deleteItem(id: string): Promise<void> {
const { $ } = await import('@xec-sh/core');
await $\`echo "Deleting item with ID: \${id}"\`;
}
`
}
},
task: {
simple: `# Simple command task
{name}:
description: {description}
command: echo "Task {name} executed!"
`,
standard: `# Standard task with parameters
{name}:
description: {description}
params:
- name: message
description: Message to display
default: "Hello from {name}"
command: |
echo "Starting {name} task..."
echo "\${params.message}"
echo "Task completed!"
`,
advanced: `# Advanced multi-step task
{name}:
description: {description}
params:
- name: target
description: Target environment
required: true
values: [development, staging, production]
- name: version
description: Version to deploy
required: true
pattern: "^v\\\\d+\\\\.\\\\d+\\\\.\\\\d+$"
# Task-specific variables
env:
LOG_LEVEL: info
DEPLOY_PATH: /opt/apps/myapp
steps:
# Step 1: Validation
- name: Validate environment
command: |
if [ "\${params.target}" = "production" ]; then
echo "⚠️ Production deployment - proceed with caution!"
fi
onFailure: abort
# Step 2: Build
- name: Build application
command: npm run build:\${params.target}
register: build_output
# Step 3: Run tests
- name: Run tests
command: npm test
when: params.target != 'production'
onFailure:
retry: 2
delay: 5s
# Step 4: Deploy to target
- name: Deploy to \${params.target}
target: hosts.\${params.target}
command: |
cd \${env.DEPLOY_PATH}
git fetch origin
git checkout \${params.version}
npm install --production
npm run migrate
pm2 reload app
alwaysRun: true
# Step 5: Health check
- name: Verify deployment
command: |
sleep 5
curl -f http://\${params.target}.example.com/health || exit 1
onFailure:
command: |
echo "Health check failed!"
# Rollback logic here
# Hooks
hooks:
before:
- command: echo "🚀 Starting deployment to \${params.target}"
after:
- command: echo "✅ Deployment completed successfully"
onError:
- command: echo "❌ Deployment failed"
- task: notify:slack
args:
message: "Deployment to \${params.target} failed"
# Success/error handlers
onSuccess:
emit: deployment:completed
onError:
emit: deployment:failed
`
},
profile: {
basic: `# Basic environment profile
{name}:
description: {description}
# Profile-specific variables
vars:
environment: {name}
log_level: info
# Environment variables
env:
NODE_ENV: {name}
LOG_LEVEL: \${vars.log_level}
`,
advanced: `# Advanced environment profile with targets
{name}:
description: {description}
extends: base # Inherit from base profile
# Profile-specific variables
vars:
environment: {name}
region: us-east-1
deploy_path: /opt/apps/myapp
log_level: info
api_url: https://api-{name}.example.com
# Environment variables
env:
NODE_ENV: {name}
API_URL: \${vars.api_url}
AWS_REGION: \${vars.region}
LOG_LEVEL: \${vars.log_level}
# Profile-specific targets
targets:
hosts:
app-server:
host: {name}.example.com
user: deploy
privateKey: ~/.ssh/{name}_rsa
containers:
app:
image: myapp:{name}
env:
- NODE_ENV=\${vars.environment}
- API_URL=\${vars.api_url}
pods:
web:
namespace: {name}
selector: app=web,env={name}
`
},
extension: {
basic: `# Basic Xec extension
name: {name}
description: {description}
version: 1.0.0
# Tasks provided by this extension
tasks:
{name}:hello:
description: Say hello from {name} extension
command: echo "Hello from {name} extension!"
{name}:info:
description: Show extension information
command: |
echo "Extension: {name}"
echo "Version: 1.0.0"
echo "Description: {description}"
# Configuration schema
config:
type: object
properties:
enabled:
type: boolean
default: true
settings:
type: object
properties:
option1:
type: string
default: "default value"
`,
advanced: `# Advanced Xec extension
name: {name}
description: {description}
version: 1.0.0
author: Your Name
# Dependencies
requires:
- xec: ">=0.7.0"
- node: ">=18.0.0"
# Tasks provided by this extension
tasks:
# Setup task
{name}:setup:
description: Setup {name} extension
params:
- name: config
description: Configuration file path
default: ./{name}.config.json
command: |
echo "Setting up {name} extension..."
if [ ! -f "\${params.config}" ]; then
echo '{
"version": "1.0.0",
"settings": {}
}' > "\${params.config}"
fi
echo "Setup complete!"
# Main task with multiple steps
{name}:run:
description: Run {name} main task
params:
- name: mode
description: Execution mode
values: [fast, normal, thorough]
default: normal
steps:
- name: Validate environment
command: |
if ! command -v node &> /dev/null; then
echo "Node.js is required but not installed"
exit 1
fi
- name: Execute main logic
script: ./scripts/{name}-main.js
env:
MODE: \${params.mode}
- name: Generate report
command: |
echo "Generating report..."
echo "Mode: \${params.mode}"
echo "Timestamp: $(date)"
# Cleanup task
{name}:clean:
description: Clean up {name} resources
command: |
echo "Cleaning up {name} resources..."
rm -rf ./{name}-temp/
rm -f ./{name}.log
echo "Cleanup complete!"
# Hooks that other tasks can use
hooks:
before_{name}:
description: Hook to run before {name} tasks
command: echo "Preparing {name} environment..."
after_{name}:
description: Hook to run after {name} tasks
command: echo "{name} task completed"
# Configuration schema
config:
type: object
required: [enabled]
properties:
enabled:
type: boolean
default: true
description: Enable or disable this extension
settings:
type: object
properties:
logLevel:
type: string
enum: [debug, info, warn, error]
default: info
timeout:
type: number
minimum: 0
default: 30
description: Task timeout in seconds
features:
type: array
items:
type: string
default: []
description: Enabled features
# Scripts included with the extension
scripts:
- scripts/{name}-main.js
- scripts/{name}-utils.js
# Documentation
docs:
readme: README.md
examples: examples/
`
}
};
async function getArtifactType() {
const type = await clack.select({
message: 'What would you like to create?',
options: [
{ value: 'project', label: '📁 Project - Initialize a new Xec project' },
{ value: 'script', label: '📜 Script - Create an executable script' },
{ value: 'command', label: '⚡ Command - Add a CLI command' },
{ value: 'task', label: '🔧 Task - Define a reusable task' },
{ value: 'profile', label: '🌍 Profile - Environment configuration' },
{ value: 'extension', label: '🧩 Extension - Create an Xec extension' },
]
});
if (InteractiveHelpers.isCancelled(type)) {
throw new Error('cancelled');
}
return type;
}
function validateName(name, type) {
if (!name)
return 'Name is required';
switch (type) {
case 'project':
if (!/^[a-z0-9-_]+$/i.test(name)) {
return 'Project name must contain only letters, numbers, hyphens, and underscores';
}
break;
case 'command':
case 'task':
case 'profile':
case 'extension':
if (!/^[a-z0-9-_:]+$/i.test(name)) {
return 'Name must contain only letters, numbers, hyphens, underscores, and colons';
}
break;
case 'script':
if (!/^[a-z0-9-_.]+$/i.test(name)) {
return 'Script name must contain only letters, numbers, hyphens, underscores, and dots';
}
if (name.includes('/') || name.includes('\\')) {
return 'Script name cannot contain path separators';
}
break;
}
return undefined;
}
async function createProject(name, options) {
const targetDir = path.resolve(name);
const xecDir = path.join(targetDir, '.xec');
if (fs.existsSync(targetDir) && !options.force) {
const files = await fs.readdir(targetDir);
if (files.length > 0) {
const shouldContinue = await clack.confirm({
message: `Directory ${name} is not empty. Continue?`,
initialValue: false
});
if (InteractiveHelpers.isCancelled(shouldContinue) || !shouldContinue) {
clack.log.info('Project creation cancelled');
return;
}
}
}
const description = options.description || await clack.text({
message: 'Project description:',
defaultValue: 'An Xec automation project'
});
if (InteractiveHelpers.isCancelled(description)) {
throw new Error('cancelled');
}
const spinner = clack.spinner();
spinner.start('Creating project structure...');
const template = options.minimal ? TEMPLATES.project.minimal : TEMPLATES.project.standard;
for (const [filePath, content] of Object.entries(template.files)) {
const fullPath = path.join(targetDir, filePath);
await fs.ensureDir(path.dirname(fullPath));
const processedContent = content
.replace(/{name}/g, path.basename(name))
.replace(/{description}/g, description);
await fs.writeFile(fullPath, processedContent);
if (filePath.includes('/scripts/') && filePath.endsWith('.js')) {
await fs.chmod(fullPath, '755');
}
}
const dirs = ['.xec/cache', '.xec/logs', '.xec/tmp'];
for (const dir of dirs) {
await fs.ensureDir(path.join(targetDir, dir));
}
spinner.stop('Project structure created');
if (!options.skipGit && !fs.existsSync(path.join(targetDir, '.git'))) {
const shouldInitGit = await clack.confirm({
message: 'Initialize git repository?',
initialValue: true
});
if (InteractiveHelpers.isCancelled(shouldInitGit)) {
return;
}
if (shouldInitGit) {
spinner.start('Initializing git repository...');
const { $ } = await import('@xec-sh/core');
await $ `cd ${targetDir} && git init`;
await $ `cd ${targetDir} && git add .`;
await $ `cd ${targetDir} && git commit -m "Initial Xec project setup"`.nothrow();
spinner.stop('Git repository initialized');
}
}
clack.outro(chalk.green('✅ Xec project initialized successfully!'));
clack.log.info('\nNext steps:');
clack.log.info(` ${chalk.cyan('cd')} ${name}`);
if (!options.minimal) {
clack.log.info(` ${chalk.cyan('xec')} scripts/example.js`);
clack.log.info(` ${chalk.cyan('xec')} hello World`);
clack.log.info(` ${chalk.cyan('xec')} tasks:run hello`);
}
clack.log.info(` ${chalk.cyan('xec')} new script my-script`);
clack.log.info(` ${chalk.cyan('xec')} new task deploy`);
}
async function createScript(name, options) {
const xecDir = path.join(process.cwd(), '.xec');
if (!fs.existsSync(xecDir)) {
clack.log.error('Not in an Xec project directory. Run "xec new project" first.');
process.exit(1);
}
const isJs = options.js ?? false;
const ext = isJs ? '.js' : '.ts';
const fileName = name.endsWith('.js') || name.endsWith('.ts') ? name : `${name}${ext}`;
const filePath = path.join(xecDir, 'scripts', fileName);
if (fs.existsSync(filePath) && !options.force) {
const shouldOverwrite = await clack.confirm({
message: `Script ${fileName} already exists. Overwrite?`,
initialValue: false
});
if (InteractiveHelpers.isCancelled(shouldOverwrite) || !shouldOverwrite) {
clack.log.info('Script creation cancelled');
return;
}
}
const description = options.description || await clack.text({
message: 'Script description:',
defaultValue: `Custom script ${name}`
});
if (InteractiveHelpers.isCancelled(description)) {
throw new Error('cancelled');
}
const templateKey = options.advanced ? 'advanced' : 'basic';
const template = TEMPLATES.script[templateKey][isJs ? 'js' : 'ts'];
const content = template
.replace(/{name}/g, path.basename(name, ext))
.replace(/{description}/g, description)
.replace(/{filepath}/g, path.relative(process.cwd(), filePath));
await fs.ensureDir(path.dirname(filePath));
await fs.writeFile(filePath, content);
await fs.chmod(filePath, '755');
clack.outro(chalk.green(`✅ Created script: ${fileName}`));
clack.log.info('\nUsage:');
clack.log.info(` ${chalk.cyan('xec')} scripts/${fileName}`);
clack.log.info(` ${chalk.cyan('xec')} scripts/${fileName} --env=production`);
}
async function createCommand(name, options) {
const xecDir = path.join(process.cwd(), '.xec');
if (!fs.existsSync(xecDir)) {
clack.log.error('Not in an Xec project directory. Run "xec new project" first.');
process.exit(1);
}
const isJs = options.js ?? false;
const ext = isJs ? '.js' : '.ts';
const fileName = `${name}${ext}`;
const filePath = path.join(xecDir, 'commands', fileName);
if (fs.existsSync(filePath) && !options.force) {
const shouldOverwrite = await clack.confirm({
message: `Command ${fileName} already exists. Overwrite?`,
initialValue: false
});
if (InteractiveHelpers.isCancelled(shouldOverwrite) || !shouldOverwrite) {
clack.log.info('Command creation cancelled');
return;
}
}
const description = options.description || await clack.text({
message: 'Command description:',
defaultValue: `Custom command ${name}`
});
if (InteractiveHelpers.isCancelled(description)) {
throw new Error('cancelled');
}
const templateKey = options.advanced ? 'advanced' : 'basic';
const template = TEMPLATES.command[templateKey][isJs ? 'js' : 'ts'];
const content = template
.replace(/{name}/g, name)
.replace(/{description}/g, description);
await fs.ensureDir(path.dirname(filePath));
await fs.writeFile(filePath, content);
clack.outro(chalk.green(`✅ Created command: ${fileName}`));
clack.log.info('\nUsage:');
clack.log.info(` ${chalk.cyan('xec')} ${name} --help`);
if (options.advanced) {
clack.log.info(` ${chalk.cyan('xec')} ${name} list`);
clack.log.info(` ${chalk.cyan('xec')} ${name} create myitem`);
}
}
async function createTask(name, options) {
const configManager = new ConfigurationManager();
const config = await configManager.load();
if (!config) {
clack.log.error('No configuration found. Run "xec new project" first.');
process.exit(1);
}
const description = options.description || await clack.text({
message: 'Task description:',
defaultValue: `Task ${name}`
});
if (InteractiveHelpers.isCancelled(description)) {
throw new Error('cancelled');
}
const complexity = options.advanced ? 'advanced' :
await clack.select({
message: 'Task complexity:',
options: [
{ value: 'simple', label: 'Simple - Single command' },
{ value: 'standard', label: 'Standard - With parameters' },
{ value: 'advanced', label: 'Advanced - Multi-step with hooks' }
]
});
if (InteractiveHelpers.isCancelled(complexity)) {
throw new Error('cancelled');
}
const template = TEMPLATES.task[complexity];
const taskYaml = template
.replace(/{name}/g, name)
.replace(/{description}/g, description);
const yaml = await import('js-yaml');
const taskConfig = yaml.load(taskYaml);
const existingTasks = configManager.get('tasks') || {};
const updatedTasks = { ...existingTasks, ...taskConfig };
configManager.set('tasks', updatedTasks);
await configManager.save();
clack.outro(chalk.green(`✅ Created task: ${name}`));
clack.log.info('\nUsage:');
clack.log.info(` ${chalk.cyan('xec')} tasks:run ${name}`);
if (complexity !== 'simple') {
clack.log.info(` ${chalk.cyan('xec')} tasks:run ${name} --help`);
}
}
async function createProfile(name, options) {
const configManager = new ConfigurationManager();
const config = await configManager.load();
if (!config) {
clack.log.error('No configuration found. Run "xec new project" first.');
process.exit(1);
}
const description = options.description || await clack.text({
message: 'Profile description:',
defaultValue: `${name} environment profile`
});
if (InteractiveHelpers.isCancelled(description)) {
throw new Error('cancelled');
}
const templateKey = options.advanced ? 'advanced' : 'basic';
const template = TEMPLATES.profile[templateKey];
const profileYaml = template
.replace(/{name}/g, name)
.replace(/{description}/g, description);
const yaml = await import('js-yaml');
const profileConfig = yaml.load(profileYaml);
const existingProfiles = configManager.get('profiles') || {};
const updatedProfiles = { ...existingProfiles, ...profileConfig };
configManager.set('profiles', updatedProfiles);
await configManager.save();
clack.outro(chalk.green(`✅ Created profile: ${name}`));
clack.log.info('\nUsage:');
clack.log.info(` ${chalk.cyan('xec')} --profile ${name} <command>`);
clack.log.info(` ${chalk.cyan('XEC_PROFILE=')}${name} xec <command>`);
}
async function createExtension(name, options) {
const targetDir = path.resolve(name);
if (fs.existsSync(targetDir) && !options.force) {
const shouldContinue = await clack.confirm({
message: `Directory ${name} already exists. Continue?`,
initialValue: false
});
if (InteractiveHelpers.isCancelled(shouldContinue) || !shouldContinue) {
clack.log.info('Extension creation cancelled');
return;
}
}
const description = options.description || await clack.text({
message: 'Extension description:',
defaultValue: `Xec extension ${name}`
});
if (InteractiveHelpers.isCancelled(description)) {
throw new Error('cancelled');
}
const spinner = clack.spinner();
spinner.start('Creating extension structure...');
const templateKey = options.advanced ? 'advanced' : 'basic';
const template = TEMPLATES.extension[templateKey];
const extensionYaml = template
.replace(/{name}/g, name)
.replace(/{description}/g, description);
await fs.ensureDir(targetDir);
await fs.writeFile(path.join(targetDir, 'extension.yaml'), extensionYaml);
if (options.advanced) {
await fs.ensureDir(path.join(targetDir, 'scripts'));
await fs.writeFile(path.join(targetDir, 'scripts', `${name}-main.js`), `#!/usr/bin/env node
// Main script for ${name} extension
const mode = process.env.MODE || 'normal';
console.log(\`Running ${name} in \${mode} mode...\`);
// Your extension logic here
`);
await fs.writeFile(path.join(targetDir, 'scripts', `${name}-utils.js`), `// Utility functions for ${name} extension
export function helper() {
return 'Helper function';
}
`);
await fs.ensureDir(path.join(targetDir, 'examples'));
await fs.writeFile(path.join(targetDir, 'examples', 'basic.yaml'), `# Example usage of ${name} extension
extensions:
- source: ./${name}
config:
enabled: true
settings:
logLevel: info
tasks:
example:
description: Example using ${name}
steps:
- task: ${name}:setup
- task: ${name}:run
args:
mode: fast
`);
await fs.writeFile(path.join(targetDir, 'README.md'), `# ${name}
${description}
## Installation
\`\`\`yaml
# In your .xec/config.yaml
extensions:
- source: path/to/${name}
config:
enabled: true
\`\`\`
## Usage
\`\`\`bash
# Setup the extension
xec tasks:run ${name}:setup
# Run the main task
xec tasks:run ${name}:run --mode=fast
\`\`\`
## Configuration
See \`extension.yaml\` for configuration options.
## Tasks
- \`${name}:setup\` - Initial setup
- \`${name}:run\` - Main task
- \`${name}:clean\` - Cleanup resources
`);
}
await fs.writeFile(path.join(targetDir, 'package.json'), JSON.stringify({
name: `xec-ext-${name}`,
version: '1.0.0',
description,
main: 'extension.yaml',
keywords: ['xec', 'extension', name],
files: ['extension.yaml', 'scripts', 'examples', 'README.md'],
engines: {
xec: '>=0.7.0'
}
}, null, 2));
spinner.stop('Extension structure created');
clack.outro(chalk.green(`✅ Created extension: ${name}`));
clack.log.info('\nNext steps:');
clack.log.info(` ${chalk.cyan('cd')} ${name}`);
clack.log.info(` ${chalk.cyan('edit')} extension.yaml`);
clack.log.info('\nTo use in a project:');
clack.log.info(` Add to .xec/config.yaml:`);
clack.log.info(` ${chalk.gray('extensions:')}`);
clack.log.info(` ${chalk.gray(' - source:')} ${targetDir}`);
}
export class NewCommand extends BaseCommand {
constructor() {
super({
name: 'new',
descr