Universal Module
Cross-cutting utilities and core functionality in Spry.
Introduction
The universal module serves as the foundation of Spry, providing cross-cutting utilities and core functionality used throughout the entire system. Think of it as the "standard library" for Spry.
Purpose
The universal module provides essential infrastructure for:
- DAG-based task execution - Build and execute dependency graphs for tasks that must run in a specific order
- Shell command execution - Run system commands with rich event tracking and output handling
- File system utilities - Work with files, paths, and directory trees
- Terminal UI components - Build rich, interactive command-line interfaces
- Configuration parsing - Handle various config formats (.gitignore, properties files, JSON schemas)
- Shared helpers - Common utilities for strings, objects, interpolation, and more
Directory Structure
lib/universal/
├── task.ts # DAG execution engine - core task orchestration
├── event-bus.ts # Typed event bus - pub/sub messaging
├── shell.ts # Shell command execution with events
├── depends.ts # Dependency resolution utilities
├── code.ts # Code block utilities
├── code-comments.ts # Comment extraction and handling
├── cline.ts # Command-line parsing for code fences
├── content-acquisition.ts # Content loading from various sources
├── text-utils.ts # String manipulation utilities
├── tmpl-literal-aide.ts # Template literal helpers
├── json-stringify-aide.ts # JSON serialization utilities
├── interpolate.ts # String interpolation utilities
├── resource.ts # Resource abstraction layer
├── path-tree.ts # File tree representation
├── path-tree-tabular.ts # Tabular display of path trees
├── gitignore.ts # .gitignore file management
├── properties.ts # Properties file parsing
├── zod-aide.ts # Zod schema utilities
├── lister-tabular-tui.ts # Tabular terminal UI builder
├── lister-tree-tui.ts # Tree-based terminal UI builder
├── task-visuals.ts # DAG visualization (ASCII, Mermaid)
├── os-user.ts # Operating system user information
├── doctor.ts # System diagnostics and health checks
├── watcher.ts # File system watching with debouncing
├── collectable.ts # Async collection utilities
├── pmd-shebang.ts # Programmable Markdown shebang handling
├── posix-pi.ts # POSIX process inspection
├── merge.ts # Deep object merging
├── reverse-proxy-simulate.ts # Reverse proxy simulation
├── version.ts # Version management from git tags
└── sql-text.ts # SQL text utilitiesCore Modules
task.ts - DAG Execution Engine
The heart of Spry's task system, implementing a Directed Acyclic Graph (DAG) execution engine that handles task dependencies and parallel execution.
Building Execution Plans
import { executionPlan, executionSubplan, executeDAG } from "./task.ts";
// Create full execution plan from all tasks
const plan = executionPlan(tasks);
// Create subplan for specific targets (useful for "spry build test")
const subplan = executionSubplan(plan, ["build", "test"]);
// Execute the plan - tasks run in dependency order with parallelization
const results = await executeDAG(plan, async (task) => {
// Your task execution logic
console.log(`Running: ${task.taskId()}`);
// Return execution result
return { success: true, output: "done" };
});Key Concepts:
- Topological Sorting - Tasks are ordered so dependencies always run first
- Parallel Execution - Independent tasks run concurrently for speed
- Cycle Detection - Circular dependencies are caught and reported
- Partial Plans - Execute only what's needed for specific targets
Task Interface
Every task must implement this interface:
interface Task {
taskId(): string; // Unique identifier
dependencies(): string[]; // Task IDs this depends on
}Event Buses for Progress Tracking
Monitor task execution with different verbosity levels:
import { verboseInfoTaskEventBus, errorOnlyTaskEventBus } from "./task.ts";
// Rich output with progress indicators, timing, and status
const bus = verboseInfoTaskEventBus({ style: "rich" });
// Minimal output - only show errors
const errorBus = errorOnlyTaskEventBus({ style: "plain" });
// Use with executeDAG
const results = await executeDAG(plan, handler, { bus });Use Cases:
- Build systems where tasks compile, test, and deploy in order
- Data pipelines where transformations depend on previous steps
- CI/CD workflows with complex dependency chains
event-bus.ts - Typed Event System
A generic, type-safe pub/sub event bus for loose coupling between components.
import { eventBus } from "./event-bus.ts";
// Define your event types - ensures type safety
type MyEvents = {
"start": { id: string; timestamp: number };
"done": { id: string; result: number; duration: number };
"error": { id: string; error: Error };
};
const bus = eventBus<MyEvents>();
// Subscribe to events - handlers are fully typed
bus.on("start", (e) => console.log(`Starting ${e.id} at ${e.timestamp}`));
bus.on("done", (e) => console.log(`Done ${e.id}: ${e.result} (${e.duration}ms)`));
bus.on("error", (e) => console.error(`Failed ${e.id}:`, e.error.message));
// Emit events - TypeScript ensures correct payload
bus.emit("start", { id: "task-1", timestamp: Date.now() });
bus.emit("done", { id: "task-1", result: 0, duration: 1250 });
// bus.emit("done", { id: "task-1" }); // TS Error: missing fields!Benefits:
- Decouples components (emitters don't know about subscribers)
- Type-safe event payloads catch bugs at compile time
- Easy to test (mock the bus)
- Multiple subscribers per event
shell.ts - Shell Command Execution
Execute shell commands with comprehensive event tracking, output capture, and multiple execution modes.
import { shell, verboseInfoShellEventBus } from "./shell.ts";
const bus = verboseInfoShellEventBus({ style: "rich" });
const sh = shell({ bus, cwd: Deno.cwd() });
// Execute single command - captures stdout/stderr
const result = await sh.spawnText("echo hello");
console.log(result.stdout); // "hello\n"
// Auto-detect execution mode from shebang
const autoResult = await sh.auto(`#!/bin/bash
echo "Hello from bash"
ls -la
`);
// Execute multi-line script via deno task
// Each line runs separately with its own events
const evalResult = await sh.denoTaskEval(`
echo "Step 1: Setup"
mkdir -p build
echo "Step 2: Compile"
deno bundle src/main.ts build/bundle.js
`);Shell Events for Monitoring
The shell emits events throughout execution, enabling rich logging and progress tracking:
| Event | Description | Payload |
|---|---|---|
spawn:start | Command starting execution | { cmd: string, cwd: string } |
spawn:done | Command completed successfully | { cmd: string, code: number, stdout: string } |
spawn:error | Command failed or errored | { cmd: string, error: Error } |
task:line:start | Eval line starting (multi-line mode) | { line: string, index: number } |
task:line:done | Eval line completed | { line: string, success: boolean } |
shebang:tempfile | Temporary script file created | { path: string, shebang: string } |
shebang:cleanup | Temporary file removed | { path: string } |
auto:mode | Execution mode detected | { mode: "shebang" | "eval" } |
Use Cases:
- Running build commands with progress tracking
- Executing test suites with detailed output
- Automating deployments with error handling
- Creating shell-based task workflows
Utility Modules
cline.ts - Code Fence Command-Line Parsing
Parses code fence info strings (like ` bash task-name --flag value ) into structured data.
import { parseCodeFenceInfo } from "./cline.ts";
const info = parseCodeFenceInfo("bash task-name --descr 'Build the app' --depends setup,test");
// Returns:
// {
// language: "bash",
// identity: "task-name",
// descr: "Build the app",
// depends: ["setup", "test"]
// }This is crucial for Spry's Markdown-based task definitions where code fences define executable tasks.
tmpl-literal-aide.ts - Template Literal Helpers
Utilities for working with template literals and multi-line strings.
import { dedentIfFirstLineBlank, indent, safeJsonStringify } from "./tmpl-literal-aide.ts";
// Remove leading whitespace (common in template literals)
const text = dedentIfFirstLineBlank(`
function hello() {
console.log("Hello");
}
`);
// Result: "function hello() {\n console.log(\"Hello\");\n}"
// Add consistent indentation
const indented = indent("line1\nline2\nline3", " ");
// Result: " line1\n line2\n line3"
// Safe JSON stringify with error handling
const json = safeJsonStringify({ nested: { data: [1, 2, 3] } });Why This Matters:
Template literals preserve indentation from your code, which looks messy in output. These helpers normalize whitespace for cleaner results.
gitignore.ts - .gitignore Management
Programmatically manage .gitignore files without duplicating entries.
import { gitignore } from "./gitignore.ts";
// Add entries only if they don't exist
const result = await gitignore("dev-src.auto", ".env", "*.tmp");
// Returns:
// {
// added: ["dev-src.auto", ".env"], // New entries
// preserved: ["*.tmp"] // Already existed
// }Use Cases:
- Auto-generating .gitignore during project setup
- Ensuring generated files are always ignored
- Tool-specific ignore patterns (e.g., Spry adds its own entries)
zod-aide.ts - Zod Schema Utilities
Bridge between JSON Schema and Zod for runtime validation.
import { jsonToZod } from "./zod-aide.ts";
// Convert JSON Schema to Zod schema
const schema = jsonToZod(`{
"type": "object",
"properties": {
"name": { "type": "string" },
"age": { "type": "number" },
"email": { "type": "string", "format": "email" }
},
"required": ["name", "age"]
}`);
// Use for runtime validation
const result = schema.safeParse({ name: "Alice", age: 30, email: "alice@example.com" });
if (result.success) {
console.log("Valid:", result.data);
} else {
console.error("Errors:", result.error.errors);
}This enables using JSON Schema definitions (which are portable and tool-agnostic) with Zod's excellent TypeScript integration.
UI Modules
lister-tabular-tui.ts - Tabular Terminal UI
Build rich, formatted tables for the terminal with custom columns and styling.
import { ListerBuilder } from "./lister-tabular-tui.ts";
type Task = { name: string; status: "done" | "running" | "pending"; duration: number };
const tasks: Task[] = [
{ name: "build", status: "done", duration: 1234 },
{ name: "test", status: "running", duration: 567 },
{ name: "deploy", status: "pending", duration: 0 }
];
await new ListerBuilder<Task>()
.declareColumns("name", "status", "duration")
.from(tasks)
.field("name", "name", { header: "Task Name" })
.field("status", "status", {
header: "Status",
format: (s) => s === "done" ? "✓" : s === "running" ? "⟳" : "○"
})
.field("duration", "duration", {
header: "Duration (ms)",
format: (d) => d > 0 ? d.toString() : "—"
})
.build()
.ls(true); // true = output to stdout
// Output:
// Task Name Status Duration (ms)
// build ✓ 1234
// test ⟳ 567
// deploy ○ —Features:
- Custom formatters for each column
- Automatic alignment and spacing
- Header customization
- Sorting and filtering support
lister-tree-tui.ts - Tree Terminal UI
Display hierarchical data as expandable trees in the terminal.
import { TreeLister, ListerBuilder } from "./lister-tree-tui.ts";
type FileRow = { path: string; size: number; type: string };
const files: FileRow[] = [
{ path: "src/main.ts", size: 1234, type: "file" },
{ path: "src/utils/helper.ts", size: 567, type: "file" },
{ path: "tests/main_test.ts", size: 890, type: "file" }
];
const base = new ListerBuilder<FileRow>()
.declareColumns("path", "size", "type")
.field("path", "path", { header: "File" })
.field("size", "size", { header: "Size" })
.field("type", "type", { header: "Type" });
const tree = TreeLister
.wrap(base)
.from(files)
.byPath({ pathKey: "path", separator: "/" })
.treeOn("path");
await tree.ls(true);
// Output:
// src/
// main.ts (1234 bytes)
// utils/
// helper.ts (567 bytes)
// tests/
// main_test.ts (890 bytes)Perfect for displaying file systems, nested configurations, or any hierarchical data.
task-visuals.ts - DAG Visualization
Visualize task dependency graphs in multiple formats.
import { executionPlanVisuals, ExecutionPlanVisualStyle } from "./task-visuals.ts";
const visuals = executionPlanVisuals(plan);
// ASCII art representation (good for terminals)
console.log(visuals.visualText(ExecutionPlanVisualStyle.ASCII));
// Output:
// setup
// ├─→ build
// │ └─→ test
// └─→ lint// Mermaid diagram (for documentation/GitHub)
console.log(visuals.visualText(ExecutionPlanVisualStyle.Mermaid));
// Output:
// graph TD
// setup --> build
// build --> test
// setup --> lintUse Cases:
- Debugging complex task dependencies
- Documenting build processes
- Visualizing CI/CD pipelines
System Modules
doctor.ts - System Diagnostics
Check if required tools and dependencies are available.
import { doctor } from "./doctor.ts";
const diags = doctor([
"deno --version",
"sqlpage --version",
"git --version"
]);
const result = await diags.run();
diags.render.cli(result);
// Output:
// ✓ deno 2.1.0
// ✗ sqlpage: command not found
// ✓ git version 2.43.0Essential for tools that depend on external commands - helps users troubleshoot missing dependencies.
watcher.ts - File System Watching
Watch files for changes and trigger rebuilds automatically.
import { watcher } from "./watcher.ts";
const run = watcher(
["Spryfile.md", "src/**/*.ts"], // Files/patterns to watch
async () => {
console.log("Files changed, rebuilding...");
await runBuild();
},
{
debounceMs: 100, // Wait 100ms after last change
ignore: ["build/", "*.tmp"]
}
);
await run(true); // true = watch mode, false = run onceDebouncing: Multiple rapid changes trigger only one rebuild after activity settles.
version.ts - Git-Based Versioning
Automatically determine version from git tags.
import { computeSemVerSync } from "./version.ts";
// Looks for git tags like v1.2.3
const version = computeSemVerSync(import.meta.url);
console.log(version); // "1.2.3"
// Use in your app
console.log(`MyTool version ${version}`);This follows semantic versioning based on your git repository's tags, ensuring version numbers stay in sync with releases.
pmd-shebang.ts - Programmable Markdown
Handle executable Markdown files with shebangs.
import { generateShebang, isExecutableMarkdown } from "./pmd-shebang.ts";
// Generate proper shebang for executable Markdown
const shebang = generateShebang("./spry.ts");
// Returns: "#!/usr/bin/env -S deno run -A ./spry.ts runbook -m"
// Check if a file is executable Markdown
const content = await Deno.readTextFile("README.md");
if (isExecutableMarkdown(content)) {
console.log("This Markdown file can be executed!");
}What This Enables:
- Markdown files that run as scripts (
./README.md) - Literate programming (documentation IS the code)
- Self-documenting build scripts
File System Modules
resource.ts - Resource Abstraction
Unified interface for file operations.
import { Resource } from "./resource.ts";
const res = new Resource("path/to/file.txt");
// Read content
const content = await res.read();
// Write content
await res.write("new content");
// Check existence
if (await res.exists()) {
console.log("File exists");
}
// Get metadata
const info = await res.stat();
console.log(`Size: ${info.size} bytes`);Abstracts away file system details, making it easier to swap between local files, remote URLs, or virtual file systems.
path-tree.ts - File Tree Representation
Build and display file system trees.
import { PathTree } from "./path-tree.ts";
const tree = new PathTree();
tree.add("src/main.ts");
tree.add("src/utils/helper.ts");
tree.add("src/utils/logger.ts");
tree.add("tests/main_test.ts");
console.log(tree.render());
// Output:
// src/
// main.ts
// utils/
// helper.ts
// logger.ts
// tests/
// main_test.tsUseful for displaying project structure, generating file lists, or visualizing changes.
content-acquisition.ts - Content Loading
Load content from various sources (files, URLs, stdin).
import { acquireContent, SourceRelativeTo } from "./content-acquisition.ts";
// Load from local file system
const local = await acquireContent(
"docs/README.md",
SourceRelativeTo.LocalFs
);
// Load from URL
const remote = await acquireContent(
"https://example.com/config.json",
SourceRelativeTo.Url
);
// Load from stdin
const stdin = await acquireContent(
"-",
SourceRelativeTo.Stdin
);Handles the complexity of different content sources with a unified API.
Integration Patterns
Task Execution + Shell + Events
Common pattern: tasks that execute shell commands with progress tracking.
import { executionPlan, executeDAG } from "./task.ts";
import { shell, verboseInfoShellEventBus } from "./shell.ts";
const shellBus = verboseInfoShellEventBus({ style: "rich" });
const sh = shell({ bus: shellBus, cwd: Deno.cwd() });
const plan = executionPlan(tasks);
const results = await executeDAG(plan, async (task) => {
// Each task executes shell commands
await sh.denoTaskEval(task.getScript());
return { success: true };
});File Watching + Task Execution
Watch files and re-run tasks on changes.
import { watcher } from "./watcher.ts";
import { executionPlan, executeDAG } from "./task.ts";
const run = watcher(
["src/**/*.ts"],
async () => {
const plan = executionPlan(tasks);
await executeDAG(plan, handler);
},
{ debounceMs: 200 }
);
await run(true);Best Practices
1. Use Event Buses for Monitoring
Don't hardcode console.log in your logic - emit events instead:
// Tight coupling
function runTask() {
console.log("Starting...");
// ...
console.log("Done!");
}// Use event bus
function runTask(bus: EventBus) {
bus.emit("start", { id: taskId });
// ...
bus.emit("done", { id: taskId });
}2. Leverage Type Safety
Use TypeScript's type system with the event bus:
// ✅ Define event types upfront
type Events = {
"build:start": { target: string };
"build:done": { target: string; artifacts: string[] };
};
const bus = eventBus<Events>();
// Now TypeScript catches payload errors!3. Cache Execution Plans
Don't rebuild execution plans unnecessarily:
// ✅ Build once, reuse
const plan = executionPlan(tasks);
// Execute multiple times
await executeDAG(plan, handler1);
await executeDAG(plan, handler2);4. Handle Shell Errors Gracefully
Always check shell command results:
const result = await sh.spawnText("some-command");
if (result.code !== 0) {
console.error(`Command failed: ${result.stderr}`);
throw new Error(`Exit code ${result.code}`);
}5. Use Debouncing for Watchers
Prevent excessive rebuilds with appropriate debounce times:
// ✅ 100-500ms is usually good
const run = watcher(files, rebuild, { debounceMs: 200 });Performance Considerations
Key Performance Points:
- Task Execution: DAG parallelization can significantly speed up builds with independent tasks
- File Watching: Debouncing prevents rebuild storms during rapid file changes
- Shell Commands: Use
spawnTextfor simple commands,denoTaskEvalfor complex scripts - Event Buses: Minimal overhead, but avoid emitting events in hot loops
Testing
# Run all universal tests
deno test lib/universal/
# Run specific test file
deno test lib/universal/task_test.ts
# Watch mode for development
deno test --watch lib/universal/
# Run with coverage
deno test --coverage=coverage lib/universal/
deno coverage coverageCommon Patterns
Pattern: Build System
// Define tasks with dependencies
const tasks = [
{ id: "clean", deps: [] },
{ id: "build", deps: ["clean"] },
{ id: "test", deps: ["build"] },
{ id: "deploy", deps: ["test"] }
];
// Execute with progress
const plan = executionPlan(tasks);
await executeDAG(plan, async (task) => {
await sh.denoTaskEval(task.script);
});Pattern: Development Watch
// Watch and rebuild automatically
await watcher(
["src/**/*.ts", "Spryfile.md"],
async () => {
const plan = executionSubplan(fullPlan, ["build"]);
await executeDAG(plan, handler);
},
{ debounceMs: 150 }
);Pattern: CLI Tool with Doctor
// Check prerequisites before running
const diags = doctor(["deno --version", "git --version"]);
const health = await diags.run();
if (health.some(d => !d.success)) {
console.error("Missing required tools!");
diags.render.cli(health);
Deno.exit(1);
}
// Proceed with main logic...Summary
The universal module is the backbone of Spry, providing battle-tested primitives for building sophisticated command-line tools and build systems. Use these modules as building blocks for your own tools and workflows.
Key Capabilities:
- DAG-based task execution with parallelization
- Type-safe event bus for component communication
- Rich shell command execution with progress tracking
- Terminal UI components for tables and trees
- File system utilities and content loading
- System diagnostics and health checks
This comprehensive module provides everything you need to build powerful CLI tools, build systems, and automation workflows in Spry.
How is this guide?
Last updated on