Skip to main content

Overview

TailStack’s automation framework provides powerful patterns for building custom scripts. By studying the included clean and install scripts, you can create your own automation tools that leverage parallel processing, system monitoring, and robust error handling.
This guide teaches you how to extend TailStack’s automation capabilities. You’ll learn the patterns and techniques used in the production scripts.

Core Patterns

TailStack automation scripts follow three fundamental patterns:

Discovery

Recursively find files/directories across the monorepo

Parallel Execution

Process multiple targets concurrently using system threads

Resource Monitoring

Track and respond to system load in real-time

Script Template

Start with this template for cross-platform scripts:
template.ps1
<#
.SYNOPSIS
    Brief description of what your script does.
.DESCRIPTION
    Detailed explanation of functionality.
#>

$ErrorActionPreference = "Stop"

# -------------------------------------------------------------------------
# 1. CONFIGURATION
# -------------------------------------------------------------------------

$ScriptName = "My Custom Script"
$MaxThreads = [Environment]::ProcessorCount * 2

# -------------------------------------------------------------------------
# 2. HELPER FUNCTIONS
# -------------------------------------------------------------------------

function Write-ColorOutput {
    param($Message, $Color = "White")
    Write-Host $Message -ForegroundColor $Color
}

# -------------------------------------------------------------------------
# 3. MAIN EXECUTION
# -------------------------------------------------------------------------

try {
    Write-ColorOutput "--- $ScriptName ---" "Cyan"
    $startTime = Get-Date
    
    # Your logic here
    
    $elapsed = (Get-Date) - $startTime
    Write-ColorOutput "Completed in $($elapsed.TotalSeconds.ToString('F2'))s" "Green"
    
} catch {
    Write-ColorOutput "[ERROR] $($_.Exception.Message)" "Red"
    exit 1
}

Common Use Cases

1. Batch File Operations

Example: Convert all TypeScript files to use ES modules
convert-to-esm.ps1
<#
.SYNOPSIS
    Converts CommonJS imports to ES module syntax.
#>

$ErrorActionPreference = "Stop"

# Find all TypeScript files
Write-Host "Scanning for TypeScript files..." -ForegroundColor Cyan
$tsFiles = Get-ChildItem -Path . -Filter "*.ts" -Recurse -File `
    | Where-Object { $_.FullName -notmatch "node_modules" }

Write-Host "Found $($tsFiles.Count) files" -ForegroundColor Yellow

# Parallel processing using Runspaces
$ConvertAction = {
    param($FilePath)
    try {
        $content = Get-Content $FilePath -Raw
        
        # Replace require() with import
        $content = $content -replace "const (\w+) = require\('([^']+)'\)", "import \$1 from '\$2'"
        
        # Replace module.exports with export
        $content = $content -replace "module\.exports = ", "export default "
        
        Set-Content $FilePath -Value $content
        return $true
    } catch {
        return $false
    }
}

$MaxThreads = [Environment]::ProcessorCount * 2
$Pool = [RunspaceFactory]::CreateRunspacePool(1, $MaxThreads)
$Pool.Open()
$Jobs = New-Object System.Collections.Generic.List[PSObject]

foreach ($file in $tsFiles) {
    $PS = [PowerShell]::Create().AddScript($ConvertAction).AddArgument($file.FullName)
    $PS.RunspacePool = $Pool
    $Jobs.Add([PSCustomObject]@{ 
        Handle = $PS.BeginInvoke()
        Instance = $PS
        Path = $file.FullName
    })
}

$completed = 0
while ($Jobs.Handle.IsCompleted -contains $false) {
    $done = ($Jobs | Where-Object { $_.Handle.IsCompleted }).Count
    Write-Progress -Activity "Converting to ESM" -Status "$done/$($tsFiles.Count)" `
        -PercentComplete (($done/$tsFiles.Count)*100)
    Start-Sleep -Milliseconds 100
}

$Jobs | ForEach-Object { 
    $_.Instance.EndInvoke($_.Handle)
    $_.Instance.Dispose() 
}
$Pool.Close()

Write-Host "Conversion complete!" -ForegroundColor Green

2. Build Orchestration

Example: Parallel build with dependency order
build-all.ps1
<#
.SYNOPSIS
    Builds all packages in dependency order with parallelization.
#>

$ErrorActionPreference = "Stop"

# Read workspace dependencies from pnpm-workspace.yaml
$workspace = Get-Content "pnpm-workspace.yaml" | ConvertFrom-Yaml
$packages = $workspace.packages | ForEach-Object { 
    Get-ChildItem -Path $_ -Filter "package.json" -Recurse 
}

# Build dependency graph
$graph = @{}
foreach ($pkg in $packages) {
    $json = Get-Content $pkg.FullName | ConvertFrom-Json
    $graph[$json.name] = @{
        Path = $pkg.DirectoryName
        Dependencies = @($json.dependencies.PSObject.Properties.Name)
    }
}

# Topological sort (simplified)
function Get-BuildOrder {
    param($Graph)
    $visited = @{}
    $order = @()
    
    function Visit($node) {
        if ($visited[$node]) { return }
        $visited[$node] = $true
        
        foreach ($dep in $Graph[$node].Dependencies) {
            if ($Graph[$dep]) { Visit $dep }
        }
        
        $order += $node
    }
    
    foreach ($node in $Graph.Keys) { Visit $node }
    return $order
}

$buildOrder = Get-BuildOrder -Graph $graph

Write-Host "Build order: $($buildOrder -join ' -> ')" -ForegroundColor Cyan

# Execute builds
foreach ($pkgName in $buildOrder) {
    $pkgPath = $graph[$pkgName].Path
    Write-Host "Building $pkgName..." -ForegroundColor Yellow
    
    Push-Location $pkgPath
    try {
        pnpm build
        Write-Host "✓ $pkgName" -ForegroundColor Green
    } catch {
        Write-Host "✗ $pkgName" -ForegroundColor Red
        exit 1
    } finally {
        Pop-Location
    }
}

Write-Host "All packages built successfully!" -ForegroundColor Green

3. Code Quality Checks

Example: Run linters across all packages
lint-all.sh
#!/bin/bash

set -euo pipefail

RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
NC='\033[0m'

echo -e "${YELLOW}Running linters across monorepo...${NC}"

# Find all package.json files
packages_file=$(mktemp)
find . -type d -name "node_modules" -prune -o -type f -name "package.json" -print > "$packages_file"

failed_packages=()

# Lint each package
while IFS= read -r package_file; do
    pkg_dir=$(dirname "$package_file")
    pkg_name=$(basename "$pkg_dir")
    
    # Check if package has lint script
    if grep -q '"lint"' "$package_file"; then
        echo -e "${YELLOW}Linting $pkg_name...${NC}"
        
        if (cd "$pkg_dir" && pnpm lint); then
            echo -e "${GREEN}✓ $pkg_name${NC}"
        else
            echo -e "${RED}✗ $pkg_name${NC}"
            failed_packages+=("$pkg_name")
        fi
    fi
done < "$packages_file"

rm -f "$packages_file"

# Summary
if [ ${#failed_packages[@]} -eq 0 ]; then
    echo -e "\n${GREEN}All lint checks passed!${NC}"
    exit 0
else
    echo -e "\n${RED}Lint failed in ${#failed_packages[@]} package(s):${NC}"
    printf '%s\n' "${failed_packages[@]}"
    exit 1
fi

Advanced Techniques

Resource Monitoring

Add system load monitoring to your scripts:
function Get-SystemLoad {
    $cpu = Get-CimInstance Win32_Processor | 
        Measure-Object -Property LoadPercentage -Average | 
        Select-Object -ExpandProperty Average
    
    $os = Get-CimInstance Win32_OperatingSystem
    $ramUsage = 100 - [math]::Round(($os.FreePhysicalMemory / $os.TotalVisibleMemorySize) * 100)
    
    return @{
        CPU = [math]::Round($cpu)
        RAM = $ramUsage
    }
}

# Usage in loop
while ($hasWork) {
    $load = Get-SystemLoad
    
    if ($load.CPU -gt 90 -or $load.RAM -gt 90) {
        Write-Host "High load detected, throttling..." -ForegroundColor Yellow
        Start-Sleep -Seconds 5
        continue
    }
    
    # Process work
}

Progress Tracking

Implement user-friendly progress displays:
$total = 100
$completed = 0

while ($completed -lt $total) {
    # Do work
    $completed++
    
    # Update progress bar
    Write-Progress `
        -Activity "Processing Items" `
        -Status "$completed of $total complete" `
        -PercentComplete (($completed / $total) * 100)
    
    Start-Sleep -Milliseconds 100
}

Write-Progress -Activity "Processing Items" -Completed

Error Handling

Robust error handling patterns:
$ErrorActionPreference = "Stop"

try {
    # Risky operation
    $result = Invoke-Command -ScriptBlock { 
        # Your code
        if ($LASTEXITCODE -ne 0) {
            throw "Command failed with exit code $LASTEXITCODE"
        }
    }
    
} catch {
    Write-Host "[ERROR] $($_.Exception.Message)" -ForegroundColor Red
    Write-Host "Location: $($_.InvocationInfo.ScriptLineNumber)" -ForegroundColor Red
    
    # Cleanup
    # ...
    
    exit 1
} finally {
    # Always executes
    Write-Host "Cleanup complete" -ForegroundColor Gray
}

Testing Scripts

Unit Testing (PowerShell)

Use Pester for PowerShell script testing:
my-script.Tests.ps1
BeforeAll {
    . $PSScriptRoot/my-script.ps1
}

Describe "MyFunction" {
    It "Should return expected value" {
        $result = MyFunction -Param "test"
        $result | Should -Be "expected"
    }
    
    It "Should handle errors" {
        { MyFunction -Param "" } | Should -Throw
    }
}
Run tests:
Invoke-Pester

Integration Testing (Bash)

Use BATS (Bash Automated Testing System):
my-script.bats
#!/usr/bin/env bats

setup() {
    # Setup test environment
    export TEST_DIR="$(mktemp -d)"
}

teardown() {
    # Cleanup
    rm -rf "$TEST_DIR"
}

@test "script executes successfully" {
    run ./my-script.sh
    [ "$status" -eq 0 ]
}

@test "script handles missing files" {
    run ./my-script.sh nonexistent.txt
    [ "$status" -eq 1 ]
}
Run tests:
bats my-script.bats

Best Practices

Always test on both Windows and Unix-like systems:Good:
# PowerShell Core works on all platforms
$files = Get-ChildItem -Path . -Recurse
Good:
# Bash script with macOS compatibility
if [[ "$OSTYPE" == "darwin"* ]]; then
    cpu_count=$(sysctl -n hw.ncpu)
else
    cpu_count=$(nproc)
fi
Avoid:
# Linux-only command
cpu_count=$(nproc)  # Fails on macOS
Use parallel processing for I/O-bound operations:
  • File operations (read/write/delete)
  • Network requests (API calls, downloads)
  • External command execution
Avoid parallel processing for:
  • CPU-intensive calculations (already uses all cores)
  • Operations requiring strict ordering
  • Resource-constrained environments
Always handle errors gracefully:
# PowerShell
$ErrorActionPreference = "Stop"

try {
    # Risky code
} catch {
    Write-Host "Error: $($_.Exception.Message)" -ForegroundColor Red
    exit 1
}
# Bash
set -euo pipefail

trap 'echo "Error at line $LINENO"' ERR
Provide clear, colored output:
  • Use colors to indicate status (green = success, red = error, yellow = warning)
  • Show progress for long-running operations
  • Display summary at completion
  • Use consistent formatting across scripts
Document your scripts thoroughly:
  • Add synopsis and description at the top
  • Document parameters and their purpose
  • Include usage examples
  • Explain complex logic with comments
  • Note any platform-specific behavior

Script Organization

Organize your custom scripts effectively:
scripts/
├── core/                    # Built-in scripts
│   ├── clean.ps1
│   ├── clean.sh
│   ├── install.ps1
│   └── install.sh
├── custom/                  # Your custom scripts
│   ├── build-all.ps1
│   ├── build-all.sh
│   ├── deploy.ps1
│   ├── deploy.sh
│   ├── test-all.ps1
│   └── test-all.sh
├── utils/                   # Shared utilities
│   ├── common.ps1           # PowerShell functions
│   └── common.sh            # Bash functions
└── README.md               # Script documentation

Shared Utilities

Create reusable utility functions:
# Source this file in other scripts: . $PSScriptRoot/utils/common.ps1

function Write-ColorOutput {
    param(
        [string]$Message,
        [string]$Color = "White"
    )
    Write-Host $Message -ForegroundColor $Color
}

function Get-MonorepoPackages {
    return Get-ChildItem -Path . -Filter "package.json" -Recurse `
        | Where-Object { $_.FullName -notmatch "node_modules" }
}

function Invoke-InParallel {
    param(
        [array]$Items,
        [scriptblock]$Action
    )
    
    $MaxThreads = [Environment]::ProcessorCount * 2
    $Pool = [RunspaceFactory]::CreateRunspacePool(1, $MaxThreads)
    $Pool.Open()
    $Jobs = @()
    
    foreach ($item in $Items) {
        $PS = [PowerShell]::Create().AddScript($Action).AddArgument($item)
        $PS.RunspacePool = $Pool
        $Jobs += @{ Handle = $PS.BeginInvoke(); Instance = $PS }
    }
    
    $Jobs | ForEach-Object { 
        $_.Instance.EndInvoke($_.Handle)
        $_.Instance.Dispose()
    }
    $Pool.Close()
}

Code Formatting

Format all code files using Prettier
find . -type f \( -name "*.ts" -o -name "*.tsx" \) \
  -not -path "*/node_modules/*" | \
  xargs -P $(nproc) -I {} prettier --write {}

Dependency Audit

Check all packages for vulnerabilities
Get-ChildItem -Filter "package.json" -Recurse | \
  ForEach-Object -Parallel {
    Push-Location $_.DirectoryName
    pnpm audit
    Pop-Location
  } -ThrottleLimit 4

License Compliance

Generate license report
pnpm licenses list --json > licenses.json
node scripts/check-licenses.js

Bundle Analysis

Analyze bundle sizes across packages
for pkg in packages/*; do
  cd "$pkg" && pnpm build && pnpm analyze
done

Debugging Scripts

Enable debug output:
# Set verbose preference
$VerbosePreference = "Continue"

# Add debug statements
Write-Verbose "Processing file: $file"
Write-Debug "Variable value: $myVar"
Use transcript:
Start-Transcript -Path "./script-log.txt"
# Your script code
Stop-Transcript
Set breakpoints in VS Code:
// .vscode/launch.json
{
  "type": "PowerShell",
  "request": "launch",
  "name": "Debug Script",
  "script": "${workspaceFolder}/scripts/my-script.ps1"
}

Clean Script

Study the parallel deletion implementation

Install Script

Learn from the load monitoring system

Automation Scripts

Understand the overall scripts architecture

GitHub Repository

Share your custom scripts with the community

Build docs developers (and LLMs) love