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:
<#
. 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
}
#!/bin/bash
# =========================================================================
# SYNOPSIS
# Brief description of what your script does.
# DESCRIPTION
# Detailed explanation of functionality.
# =========================================================================
set -euo pipefail
# -------------------------------------------------------------------------
# 1. CONFIGURATION & COLORS
# -------------------------------------------------------------------------
RED = '\033[0;31m'
GREEN = '\033[0;32m'
YELLOW = '\033[0;33m'
CYAN = '\033[0;36m'
NC = '\033[0m' # No Color
SCRIPT_NAME = "My Custom Script"
# Detect CPU count
if [[ " $OSTYPE " == "darwin" * ]]; then
CPU_COUNT = $( sysctl -n hw.ncpu )
else
CPU_COUNT = $( nproc )
fi
MAX_THREADS = $(( CPU_COUNT * 2 ))
# -------------------------------------------------------------------------
# 2. HELPER FUNCTIONS
# -------------------------------------------------------------------------
log_info () {
echo -e "${ CYAN } $1 ${ NC }"
}
log_success () {
echo -e "${ GREEN } $1 ${ NC }"
}
log_error () {
echo -e "${ RED } $1 ${ NC }"
}
# -------------------------------------------------------------------------
# 3. MAIN EXECUTION
# -------------------------------------------------------------------------
log_info "--- $SCRIPT_NAME ---"
start_time = $( date +%s )
# Your logic here
end_time = $( date +%s )
elapsed = $(( end_time - start_time ))
log_success "Completed in ${ elapsed }s"
Common Use Cases
1. Batch File Operations
Example: Convert all TypeScript files to use ES modules
<#
. 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
#!/bin/bash
set -euo pipefail
CYAN = '\033[0;36m'
GREEN = '\033[0;32m'
YELLOW = '\033[0;33m'
NC = '\033[0m'
echo -e "${ CYAN }Scanning for TypeScript files...${ NC }"
# Find all TypeScript files (excluding node_modules)
ts_files = $( mktemp )
find . -type d -name "node_modules" -prune -o -type f -name "*.ts" -print > " $ts_files "
file_count = $( wc -l < " $ts_files " )
echo -e "${ YELLOW }Found $file_count files${ NC }"
# Conversion function
convert_file () {
local file = $1
# Replace require() with import
sed -i.bak -E "s/const ([a-zA-Z0-9_]+) = require\('([^']+)'\)/import \1 from '\2'/g" " $file "
# Replace module.exports with export
sed -i.bak "s/module\.exports = /export default /g" " $file "
# Remove backup file
rm -f "${ file }.bak"
}
export -f convert_file
# Parallel processing using xargs
if [[ " $OSTYPE " == "darwin" * ]]; then
cpu_count = $( sysctl -n hw.ncpu )
else
cpu_count = $( nproc )
fi
max_threads = $(( cpu_count * 2 ))
cat " $ts_files " | xargs -P " $max_threads " -I {} bash -c 'convert_file "{}"'
rm -f " $ts_files "
echo -e "${ GREEN }Conversion complete!${ NC }"
2. Build Orchestration
Example: Parallel build with dependency order
<#
. 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
#!/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:
PowerShell Monitoring
Bash Monitoring
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:
PowerShell Progress
Bash Progress
$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:
PowerShell Error Handling
Bash Error Handling
$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:
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:
Integration Testing (Bash)
Use BATS (Bash Automated Testing System):
#!/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:
Best Practices
1. Cross-Platform Compatibility
2. Performance Optimization
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:
utils/common.ps1
utils/common.sh
# 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 ()
}
Examples Gallery
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"
}
Enable debug mode: # Print each command before execution
set -x
# Your script code
# Disable debug
set +x
Add debug statements: DEBUG = ${ DEBUG :- 0 }
debug () {
if [ $DEBUG -eq 1 ]; then
echo "[DEBUG] $1 " >&2
fi
}
debug "Processing file: $file "
Run with debug:
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