Script Execution Context
Every Xec script runs within a rich execution context that provides access to targets, configuration, and utility functions. This guide explains the execution context and how to leverage it effectively.
Understanding
Xec provides two primary execution engines in your scripts:
// $target - Executes on the configured target (local, SSH, Docker, or Kubernetes)
await $target`ls -la`;
// $ - Always executes locally, regardless of target
await $`pwd`;
$target and $targetInfo are bound to whatever the CLI ran the script
against: xec on <host> script.js and xec in <container> script.js bind
them to that host or container, while xec run script.js — and its direct
shorthand xec script.js — bind them to the local machine
($targetInfo.type === 'local'), and -e evaluation gets the same
local binding. Code that may also run outside the CLI — under plain
node — should still check with typeof, since there the variables
aren't declared at all.
Global Context Variables
When a script executes, Xec injects these globals:
// Primary execution engines
$target // CallableExecutionEngine - bound to the current target (see above)
$targetInfo // TargetInfo - metadata about that target, present alongside $target
$ // CallableExecutionEngine - local command execution, always present
Script arguments and location are globals too: args is exactly what the
script was invoked with, argv follows the shell convention (interpreter,
script path, then the arguments), and __filename/__dirname name the
script's own location. xec run deploy.js staging --force gives
args = ['staging', '--force'] — flags the run command owns itself go
after --.
Target Information
The $targetInfo object provides details about the execution target:
interface TargetInfo {
type: 'local' | 'ssh' | 'docker' | 'kubernetes';
name?: string;
host?: string; // SSH targets
container?: string; // Docker targets
pod?: string; // Kubernetes targets
namespace?: string; // Kubernetes targets
config: any; // Full target configuration
}
Example usage:
if (typeof $targetInfo !== 'undefined') {
console.log(`Executing on ${$targetInfo.type} target: ${$targetInfo.name}`);
switch ($targetInfo.type) {
case 'ssh':
console.log(`Connected to: ${$targetInfo.host}`);
break;
case 'docker':
console.log(`Container: ${$targetInfo.container}`);
break;
case 'kubernetes':
console.log(`Pod: ${$targetInfo.pod} in namespace ${$targetInfo.namespace}`);
break;
}
}
Utility Functions
The CLI also makes a set of scripting utilities from @xec-sh/ops
available as globals in every execution path (xec run, xec script.js,
-e, the REPL), without an import — among them glob, which, fs,
os, path, sleep, retry, within, template, parseArgs, yaml,
csv, diff, and terminal helpers log/echo/prism/kit/spinner. See
TypeScript Configuration
for the full list and how to get IntelliSense for it. chalk is not one of
them — import it normally if you use it (import chalk from 'chalk').
// File globbing
const files = await glob('**/*.js');
const configs = await glob('config/*.yaml');
glob answers paths relative to cwd, separated by / on every platform
— including Windows, where the result can be fed straight back into another
pattern. A single * never crosses a directory boundary; ** does. Pass
{ absolute: true } for native paths instead, since those go to the
filesystem.
On Windows, results previously came back with \ separators, which also
meant a single * matched across directories there. Both are fixed; code
that spliced the old separator back together needs no adjustment, but code
comparing against a \-separated literal does.
Working with Multiple Targets
Scripts can be executed against different targets:
# Execute on SSH target
xec on production script.js
# Execute on Docker container
xec in my-container script.js
# Execute on Kubernetes pod
xec in pods.my-pod script.js
In the script:
// script.js
if ($targetInfo?.type === 'ssh') {
// SSH-specific logic
await $target`sudo systemctl restart nginx`;
} else if ($targetInfo?.type === 'docker') {
// Docker-specific logic
await $target`apt-get update && apt-get install -y curl`;
} else if ($targetInfo?.type === 'kubernetes') {
// Kubernetes-specific logic — runs inside the pod
await $target`cat /etc/resolv.conf`;
} else {
// Local execution
await $`echo "Running locally"`;
}
// Always execute locally regardless of target
await $`echo "This always runs on the host"`;
Parameter Parsing
Command-line parameters aren't parsed automatically — a script gets its raw
arguments as the args global and parses them itself. parseArgs, one of
the globals from @xec-sh/ops (see Utility Functions
above), covers the common --key=value and --flag cases:
// Script called with: xec deploy.js --env=prod --version=1.2.3 --force
const params = parseArgs(args);
console.log(params.env); // 'prod'
console.log(params.version); // '1.2.3'
console.log(params.force); // true
console.log(params._); // [] — positional arguments land here
// Values are always strings or booleans — parseArgs does not coerce
// numbers, JSON, or anything else. Parse those yourself if you need them.
The flags run owns — --dry-run, --watch, -o/--output,
-c/--config, --typescript, --runtime, -p/--param, the root
-v/-q — are consumed by the CLI; everything else passes through. Put an
owned flag after -- to reach the script:
xec run deploy.js -- --dry-run.
Context Isolation
Each CLI invocation is its own process, and the globals Xec injects
($target, $targetInfo, the utility functions) are restored to their
previous values after the script finishes. Variables your script assigns to
globalThis itself are not tracked — in --watch mode, where the file is
re-run inside the same process, they survive between runs.
REPL Context
When running in REPL mode, additional helpers are available:
// Start REPL
xec run --repl
// In REPL:
> .help // Show available commands
> .clear // Clear the console
> await $`ls` // Execute commands
.help and .clear are Node REPL dot-commands (replServer.defineCommand),
not callable functions — they're typed without parentheses.
Custom Context Extension
Scripts can extend their context programmatically:
// extend-context.js
import { $ } from '@xec-sh/core';
// Add custom utilities to the context
global.utils = {
async deployToAll(targetNames) {
for (const name of targetNames) {
console.log(`Deploying to ${name}...`);
await $`xec on ${name} "npm run deploy"`;
}
},
formatBytes(bytes) {
const sizes = ['B', 'KB', 'MB', 'GB'];
if (bytes === 0) return '0 B';
const i = Math.floor(Math.log(bytes) / Math.log(1024));
return Math.round(bytes / Math.pow(1024, i) * 100) / 100 + ' ' + sizes[i];
}
};
// Use the extended context
await utils.deployToAll(['staging', 'production']);
Environment Variables
Scripts inherit the process environment. Xec itself reads a few variables rather than setting them:
// Variables Xec reads
process.env.XEC_DEBUG // 'true' enables verbose/debug output
process.env.XEC_CONFIG // path to a config file to load
process.env.XEC_PROFILE // configuration profile to activate
// Any other XEC_* variable overrides configuration by path:
// XEC_VARS_APP_NAME=web -> vars.app_name = 'web'
// Pass environment variables to commands
process.env.API_KEY = 'secret';
await $`echo $API_KEY`;
// Or use env option
await $`node script.js`.env({ API_KEY: 'secret' });
Best Practices
-
Check for target availability:
if (typeof $target !== 'undefined') {// Target-specific code} -
Use type guards for target types:
if ($targetInfo?.type === 'ssh') {// SSH-specific operations} -
Provide fallbacks for local execution:
const engine = $target || $;await engine`ls -la`; -
Clean up resources:
try {// Your script logic} finally {// Cleanup code runs even on error} -
Document expected parameters:
// deploy.js// Usage: xec deploy.js --env=<environment> --version=<version>const params = parseArgs(args);if (!params.env || !params.version) {console.error('Required parameters: --env and --version');process.exit(1);}
Complete Example
Here's a comprehensive example using the execution context:
// multi-target-deploy.js
import { $ } from '@xec-sh/core';
import chalk from 'chalk';
async function main() {
// Check if running against a target (typeof guard: the variable is not
// declared at all when the file is run directly as `xec script.js`)
if (typeof $targetInfo !== 'undefined' && $targetInfo.type !== 'local') {
console.log(chalk.blue(`Deploying to ${$targetInfo.type} target: ${$targetInfo.name}`));
// Target-specific deployment
switch ($targetInfo.type) {
case 'ssh':
await deployToSSH();
break;
case 'docker':
await deployToDocker();
break;
case 'kubernetes':
await deployToKubernetes();
break;
default:
await deployLocal();
}
} else {
// Local target (xec run) or direct invocation — deploy locally
await deployLocal();
}
// Always run post-deployment tasks locally
await $`echo "Deployment complete" >> deployment.log`;
await $`date >> deployment.log`;
}
async function deployToSSH() {
console.log(`Connecting to ${$targetInfo.host}...`);
await $target`cd /app && git pull`;
await $target`npm install`;
await $target`npm run build`;
await $target`sudo systemctl restart app`;
}
async function deployToDocker() {
console.log(`Deploying to container ${$targetInfo.container}...`);
await $target`apt-get update`;
await $target`cd /app && npm install`;
await $target`npm run build`;
}
async function deployToKubernetes() {
console.log(`Deploying to pod ${$targetInfo.pod}...`);
await $target`cd /app && npm ci`;
await $target`npm run build`;
// Restart the pod
await $`kubectl rollout restart deployment/${$targetInfo.pod}`;
}
async function deployLocal() {
console.log('Deploying locally...');
await $`npm install`;
await $`npm run build`;
await $`npm run start`;
}
// Execute with error handling
try {
await main();
console.log(chalk.green('✅ Deployment successful!'));
} catch (error) {
console.error(chalk.red('❌ Deployment failed:'), error.message);
process.exit(1);
}
Run this script with different targets:
# Deploy locally
xec run multi-target-deploy.js
# Deploy to SSH server
xec on hosts.production multi-target-deploy.js
# Deploy to Docker container
xec in containers.app multi-target-deploy.js
# Deploy to Kubernetes pod
xec in pods.app multi-target-deploy.js