PTY abstraction
Execute shell commands through the pseudo-terminal abstraction
PTY Abstraction
The PTY (Pseudo-Terminal) abstraction is how plugins execute shell commands. Instead of using Node's child_process directly, you use getPty() to get a PTY instance that handles stdout/stderr streaming, error handling, and privilege escalation.
Why PTY Instead of child_process?
- Interactive Commands: PTY supports commands that require user input (like installers)
- Output Streaming: Real-time stdout/stderr is streamed to the user's terminal
- Privilege Escalation: Automatic handling of
sudopassword prompts - Context Awareness: Different PTY implementations for planning vs applying
Getting the PTY Instance
import { getPty } from '@codifycli/plugin-core';
const pty = getPty();The getPty() function uses async local storage to provide the correct PTY instance for the current context:
- BackgroundPty during
refresh()- Allows parallel execution for faster planning - SequentialPty during
create(),modify(),destroy()- Executes commands sequentially with proper error handling
spawn() - Execute with Error Handling
Use spawn() when you expect the command to succeed. If it fails (non-zero exit code), an error is thrown and execution stops.
const pty = getPty();
// Throws on non-zero exit code
const result = await pty.spawn('brew install jq');
console.log(result.data); // stdout output as stringWhen to use spawn():
- During
create(),modify(),destroy()when failure should stop execution - When the command must succeed for the operation to be valid
- When you want automatic error propagation
spawnSafe() - Execute Without Throwing
Use spawnSafe() when you need to check if something exists or when failure is an expected outcome.
const pty = getPty();
// Never throws - returns status object
const result = await pty.spawnSafe('which jq');
if (result.status === SpawnStatus.SUCCESS) {
console.log('jq is installed at:', result.data);
} else {
console.log('jq not found');
}When to use spawnSafe():
- During
refresh()to check if resources exist - When checking prerequisites or system state
- When multiple outcomes are valid
Spawn Options
Both spawn() and spawnSafe() accept an options object:
await pty.spawn('npm install', {
// Working directory for the command
cwd: '/path/to/project',
// Environment variables (merged with current env)
env: { NODE_ENV: 'production' },
// Allow interactive input (prompts, confirmations)
interactive: true,
// Require root/sudo privileges
requiresRoot: true,
// Provide input to stdin
stdin: true,
});Option Details
cwd - Change working directory:
// Clone a repo into a specific directory
await pty.spawn('git clone https://github.com/user/repo.git', {
cwd: '/Users/john/projects'
});env - Set environment variables:
// Run installer non-interactively
await pty.spawn('./install.sh', {
env: { NONINTERACTIVE: 1 }
});interactive - Allow user input:
// Run installer that may prompt for choices
await pty.spawn('/bin/bash -c "$(curl -fsSL https://install.sh)"', {
interactive: true,
stdin: true
});requiresRoot - Execute with sudo:
// Install to system directory
await pty.spawn('cp binary /usr/local/bin/', {
requiresRoot: true
});When requiresRoot: true, Codify:
- Sends a message to the parent CLI process
- CLI prompts user for their sudo password
- Password is sent back to plugin securely
- Command executes with sudo
This ensures plugins never have direct access to sudo—they must request it through the parent process.
Real-World Examples
Checking if a tool is installed:
async refresh(parameters: Partial<MyConfig>): Promise<Partial<MyConfig> | null> {
const pty = getPty();
// Check if tool exists
const result = await pty.spawnSafe('which my-tool');
if (result.status === SpawnStatus.ERROR) {
return null; // Not installed
}
// Get version
const versionResult = await pty.spawnSafe('my-tool --version');
const version = versionResult.data.match(/v(\d+\.\d+\.\d+)/)?.[1];
return { version };
}Installing with platform-specific commands:
async create(plan: CreatePlan<MyConfig>): Promise<void> {
const pty = getPty();
if (Utils.isMacOS()) {
await pty.spawn('brew install my-tool');
} else if (Utils.isLinux()) {
await pty.spawn('apt-get install -y my-tool', {
requiresRoot: true
});
}
}Running commands in a specific directory:
async create(plan: CreatePlan<ProjectConfig>): Promise<void> {
const pty = getPty();
const projectPath = plan.desiredConfig.directory;
// Create directory
await pty.spawn(`mkdir -p ${projectPath}`);
// Initialize project in that directory
await pty.spawn('npm init -y', { cwd: projectPath });
await pty.spawn('npm install express', { cwd: projectPath });
}PTY Implementation Details
Codify uses two PTY implementations:
BackgroundPty (during planning):
- Allows parallel command execution for faster refresh operations
- Automatically killed after planning completes
- Used in
refresh(),validate()
SequentialPty (during apply):
- Executes commands one at a time, in order
- Ensures proper error handling and output streaming
- Used in
create(),modify(),destroy()
You don't need to worry about which PTY you're using—getPty() provides the correct one automatically based on context.