TypeScript Configuration for Xec Scripts
Xec provides native TypeScript support with zero configuration required. This guide covers TypeScript setup, type definitions, and best practices for type-safe scripting.
Zero-Configuration TypeScript
Xec automatically compiles TypeScript files without any setup:
# JavaScript files work as-is
xec run script.js
# TypeScript files are automatically compiled
xec run script.ts
# Even TSX files are supported
xec run component.tsx
Built-in Type Definitions
Xec provides comprehensive type definitions through the @xec-sh/core
package:
import { $, ProcessPromise, ProcessOutput } from '@xec-sh/core';
import type { ExecutionEngine, ExecutionOptions } from '@xec-sh/core';
// Fully typed command execution
const result: ProcessOutput = await $`ls -la`;
const promise: ProcessPromise = $`echo "test"`;
Creating a TypeScript Project
Basic Setup
- Initialize your project:
npm init -y
npm install --save-dev typescript @types/node
npm install @xec-sh/core
- Create
tsconfig.json
:
{
"compilerOptions": {
"target": "ES2022",
"module": "ES2022",
"lib": ["ES2022"],
"moduleResolution": "node",
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"allowJs": true,
"types": ["node", "@xec-sh/core"]
},
"include": ["**/*.ts", "**/*.js"],
"exclude": ["node_modules", "dist"]
}
- Create your first TypeScript script:
// deploy.ts
import { $ } from '@xec-sh/core';
import type { ProcessOutput } from '@xec-sh/core';
interface DeployConfig {
environment: 'dev' | 'staging' | 'prod';
version: string;
dryRun?: boolean;
}
async function deploy(config: DeployConfig): Promise<void> {
const { environment, version, dryRun = false } = config;
if (dryRun) {
console.log(`[DRY RUN] Would deploy ${version} to ${environment}`);
return;
}
const result: ProcessOutput = await $`git tag v${version}`;
console.log(`Tagged version: v${version}`);
await $`npm run deploy:${environment}`;
console.log(`Deployed to ${environment}`);
}
// Type-safe argument parsing
const config: DeployConfig = {
environment: (args[0] as DeployConfig['environment']) || 'dev',
version: args[1] || '1.0.0',
dryRun: args.includes('--dry-run')
};
await deploy(config);
Type Definitions for Global Context
Xec injects global variables that TypeScript needs to know about:
Global Type Declarations
Create xec.d.ts
in your project root:
// xec.d.ts
import type { ExecutionEngine } from '@xec-sh/core';
declare global {
// Script execution context
const $target: ExecutionEngine;
const $targetInfo: {
type: 'local' | 'ssh' | 'docker' | 'k8s';
name?: string;
host?: string;
container?: string;
pod?: string;
namespace?: string;
config: any;
} | undefined;
// Script metadata
const args: string[];
const argv: string[];
const params: Record<string, any>;
const __filename: string;
const __dirname: string;
const __script: {
path: string;
args: string[];
target?: any;
};
// Configuration access
const config: {
get(path?: string): any;
reload(): Promise<void>;
};
const vars: Record<string, any>;
// Task and target APIs
const tasks: {
run(name: string, params?: Record<string, any>): Promise<any>;
list(): Promise<string[]>;
exists(name: string): Promise<boolean>;
};
const targets: {
list(type?: string): Promise<string[]>;
get(name: string): Promise<any>;
execute(name: string, command: string): Promise<any>;
};
// Utilities
const chalk: any;
function glob(pattern: string): Promise<string[]>;
function minimatch(path: string, pattern: string): boolean;
}
export {};
Advanced Type Patterns
Custom Command Builders
import { $, ProcessPromise } from '@xec-sh/core';
class CommandBuilder {
private options: {
cwd?: string;
env?: Record<string, string>;
timeout?: number;
} = {};
setCwd(dir: string): this {
this.options.cwd = dir;
return this;
}
setEnv(env: Record<string, string>): this {
this.options.env = { ...this.options.env, ...env };
return this;
}
setTimeout(ms: number): this {
this.options.timeout = ms;
return this;
}
async execute(command: string): Promise<ProcessOutput> {
let promise: ProcessPromise = $`${command}`;
if (this.options.cwd) {
promise = promise.cwd(this.options.cwd);
}
if (this.options.env) {
promise = promise.env(this.options.env);
}
if (this.options.timeout) {
promise = promise.timeout(this.options.timeout);
}
return await promise;
}
}
// Usage
const builder = new CommandBuilder()
.setCwd('/app')
.setEnv({ NODE_ENV: 'production' })
.setTimeout(30000);
await builder.execute('npm install');
Result Type Guards
import { $ } from '@xec-sh/core';
import type { ProcessOutput } from '@xec-sh/core';
interface SuccessResult extends ProcessOutput {
exitCode: 0;
}
interface ErrorResult extends ProcessOutput {
exitCode: number;
stderr: string;
}
function isSuccess(result: ProcessOutput): result is SuccessResult {
return result.exitCode === 0;
}
function isError(result: ProcessOutput): result is ErrorResult {
return result.exitCode !== 0;
}
// Usage with type narrowing
const result = await $`npm test`.nothrow();
if (isSuccess(result)) {
// TypeScript knows exitCode is 0
console.log('Tests passed:', result.stdout);
} else if (isError(result)) {
// TypeScript knows exitCode is non-zero
console.error(`Tests failed with code ${result.exitCode}:`, result.stderr);
}
Generic Command Wrappers
import { $ } from '@xec-sh/core';
async function runWithRetry<T>(
command: () => Promise<T>,
maxRetries: number = 3,
delay: number = 1000
): Promise<T> {
let lastError: Error | undefined;
for (let i = 0; i < maxRetries; i++) {
try {
return await command();
} catch (error) {
lastError = error as Error;
console.log(`Attempt ${i + 1} failed, retrying in ${delay}ms...`);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
throw lastError;
}
// Usage
const result = await runWithRetry(
() => $`curl https://api.example.com`,
5,
2000
);
Module Imports and NPM Packages
Dynamic Imports with Types
// Dynamic import with type assertion
const lodash = await import('lodash') as typeof import('lodash');
const result = lodash.uniq([1, 2, 2, 3]);
// Using the 'use' function with types
declare function use<T = any>(specifier: string): Promise<T>;
interface LodashModule {
uniq<T>(array: T[]): T[];
debounce<F extends (...args: any[]) => any>(func: F, wait: number): F;
}
const _ = await use<LodashModule>('lodash');
const unique = _.uniq([1, 2, 2, 3]);
NPM Package Types
// Install type definitions
// npm install --save-dev @types/node-fetch
import fetch from 'node-fetch';
interface ApiResponse {
id: number;
name: string;
status: 'active' | 'inactive';
}
async function fetchData(): Promise<ApiResponse> {
const response = await fetch('https://api.example.com/data');
return await response.json() as ApiResponse;
}
Configuration Types
Typed Configuration Access
interface XecConfig {
targets: {
[key: string]: {
type: 'ssh' | 'docker' | 'k8s';
host?: string;
username?: string;
container?: string;
pod?: string;
namespace?: string;
};
};
tasks: {
[key: string]: {
description?: string;
command?: string;
script?: string;
steps?: Array<{ name: string; command: string }>;
};
};
vars: Record<string, any>;
}
// Type-safe configuration access
const config = global.config as {
get<K extends keyof XecConfig>(key: K): XecConfig[K];
get(path: string): any;
reload(): Promise<void>;
};
const targets = config.get('targets');
const sshTarget = targets['production'];
if (sshTarget.type === 'ssh') {
console.log(`SSH host: ${sshTarget.host}`);
}
Error Handling with Types
Custom Error Types
class DeploymentError extends Error {
constructor(
message: string,
public readonly stage: string,
public readonly exitCode?: number
) {
super(message);
this.name = 'DeploymentError';
}
}
async function deployWithStages(): Promise<void> {
try {
await $`npm run build`;
} catch (error) {
throw new DeploymentError(
'Build failed',
'build',
(error as any).exitCode
);
}
try {
await $`npm run test`;
} catch (error) {
throw new DeploymentError(
'Tests failed',
'test',
(error as any).exitCode
);
}
}
// Usage
try {
await deployWithStages();
} catch (error) {
if (error instanceof DeploymentError) {
console.error(`Deployment failed at ${error.stage}: ${error.message}`);
process.exit(error.exitCode || 1);
}
throw error;
}
IDE Integration
VS Code Setup
- Install the TypeScript extension (built-in)
- Create
.vscode/settings.json
:
{
"typescript.tsdk": "node_modules/typescript/lib",
"typescript.enablePromptUseWorkspaceTsdk": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
}
}
- Create
.vscode/launch.json
for debugging:
{
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Debug Xec Script",
"program": "${workspaceFolder}/node_modules/.bin/xec",
"args": ["run", "${file}"],
"skipFiles": ["<node_internals>/**"],
"console": "integratedTerminal"
}
]
}
Testing TypeScript Scripts
Unit Testing with Jest
// math.ts
export function add(a: number, b: number): number {
return a + b;
}
// math.test.ts
import { add } from './math';
describe('Math functions', () => {
test('adds two numbers', () => {
expect(add(2, 3)).toBe(5);
});
});
Integration Testing
// deploy.test.ts
import { $ } from '@xec-sh/core';
import { deploy } from './deploy';
jest.mock('@xec-sh/core', () => ({
$: jest.fn()
}));
describe('Deployment', () => {
test('deploys to staging', async () => {
const mockExec = $ as jest.MockedFunction<typeof $>;
mockExec.mockResolvedValue({
stdout: 'Success',
stderr: '',
exitCode: 0,
signal: null,
duration: 100
} as any);
await deploy({ environment: 'staging', version: '1.0.0' });
expect(mockExec).toHaveBeenCalledWith(
expect.arrayContaining(['npm run deploy:staging'])
);
});
});
Best Practices
-
Use strict TypeScript settings:
{
"compilerOptions": {
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true
}
} -
Type your function parameters:
async function deploy(env: string, version: string): Promise<void> {
// Implementation
} -
Use interface for complex configs:
interface Config {
required: string;
optional?: string;
nested: {
value: number;
};
} -
Leverage type inference:
// Let TypeScript infer the type
const result = await $`ls`;
// Instead of
const result: ProcessOutput = await $`ls`; -
Use const assertions for literals:
const config = {
env: 'production',
debug: false
} as const;
Complete TypeScript Example
Here's a comprehensive TypeScript script showcasing best practices:
// release.ts - Complete release automation script
import { $, ProcessOutput } from '@xec-sh/core';
import chalk from 'chalk';
import * as semver from 'semver';
interface ReleaseOptions {
type: 'major' | 'minor' | 'patch';
dryRun?: boolean;
skipTests?: boolean;
skipChangelog?: boolean;
}
interface PackageJson {
name: string;
version: string;
scripts?: Record<string, string>;
}
class ReleaseManager {
constructor(
private readonly options: ReleaseOptions
) {}
async execute(): Promise<void> {
try {
console.log(chalk.blue('🚀 Starting release process...'));
const currentVersion = await this.getCurrentVersion();
const newVersion = this.calculateNewVersion(currentVersion);
console.log(chalk.gray(`Current version: ${currentVersion}`));
console.log(chalk.green(`New version: ${newVersion}`));
if (this.options.dryRun) {
console.log(chalk.yellow('DRY RUN - No changes will be made'));
}
await this.runPreReleaseChecks();
await this.updateVersion(newVersion);
await this.generateChangelog(newVersion);
await this.createGitTag(newVersion);
await this.publishPackage();
console.log(chalk.green('✅ Release completed successfully!'));
} catch (error) {
console.error(chalk.red('❌ Release failed:'), error);
process.exit(1);
}
}
private async getCurrentVersion(): Promise<string> {
const packageContent = await $`cat package.json`.quiet();
const packageJson: PackageJson = JSON.parse(packageContent.stdout);
return packageJson.version;
}
private calculateNewVersion(current: string): string {
const version = semver.inc(current, this.options.type);
if (!version) {
throw new Error(`Invalid version increment: ${current} -> ${this.options.type}`);
}
return version;
}
private async runPreReleaseChecks(): Promise<void> {
// Check git status
const status = await $`git status --porcelain`.nothrow();
if (status.stdout.trim() && !this.options.dryRun) {
throw new Error('Working directory is not clean');
}
// Run tests
if (!this.options.skipTests) {
console.log(chalk.yellow('Running tests...'));
await $`npm test`.pipe(process.stdout);
}
// Build project
console.log(chalk.yellow('Building project...'));
await $`npm run build`.pipe(process.stdout);
}
private async updateVersion(version: string): Promise<void> {
if (this.options.dryRun) {
console.log(chalk.gray(`Would update version to ${version}`));
return;
}
await $`npm version ${version} --no-git-tag-version`;
}
private async generateChangelog(version: string): Promise<void> {
if (this.options.skipChangelog) {
return;
}
console.log(chalk.yellow('Generating changelog...'));
if (this.options.dryRun) {
console.log(chalk.gray('Would generate changelog'));
return;
}
// Get commits since last tag
const lastTag = await $`git describe --tags --abbrev=0`.nothrow();
const range = lastTag.exitCode === 0
? `${lastTag.stdout.trim()}..HEAD`
: 'HEAD';
const commits = await $`git log ${range} --pretty=format:"- %s"`;
// Update CHANGELOG.md
const date = new Date().toISOString().split('T')[0];
const entry = `## [${version}] - ${date}\n\n${commits.stdout}\n\n`;
await $`echo "${entry}" | cat - CHANGELOG.md > temp && mv temp CHANGELOG.md`;
}
private async createGitTag(version: string): Promise<void> {
if (this.options.dryRun) {
console.log(chalk.gray(`Would create git tag v${version}`));
return;
}
await $`git add .`;
await $`git commit -m "Release v${version}"`;
await $`git tag -a v${version} -m "Release v${version}"`;
}
private async publishPackage(): Promise<void> {
if (this.options.dryRun) {
console.log(chalk.gray('Would publish to npm'));
return;
}
console.log(chalk.yellow('Publishing to npm...'));
await $`npm publish`;
console.log(chalk.yellow('Pushing to git...'));
await $`git push origin main --tags`;
}
}
// Parse command-line arguments
function parseArgs(): ReleaseOptions {
const type = (args[0] as ReleaseOptions['type']) || 'patch';
if (!['major', 'minor', 'patch'].includes(type)) {
console.error(chalk.red(`Invalid release type: ${type}`));
console.log('Usage: xec run release.ts [major|minor|patch] [--dry-run] [--skip-tests]');
process.exit(1);
}
return {
type,
dryRun: args.includes('--dry-run'),
skipTests: args.includes('--skip-tests'),
skipChangelog: args.includes('--skip-changelog')
};
}
// Main execution
const options = parseArgs();
const manager = new ReleaseManager(options);
await manager.execute();
This comprehensive example demonstrates:
- Complete TypeScript class structure
- Interface definitions for type safety
- Error handling with proper types
- Command-line argument parsing
- Integration with npm packages (semver)
- Dry-run mode for testing
- Git operations and tagging
- Changelog generation
- npm publishing workflow