Skip to content

universal Module

The universal module serves as the foundation of Spry, . Think of it as the “standard library” for Spry.

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
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 utilities

The heart of Spry’s task system, implementing a Directed Acyclic Graph (DAG) execution engine that handles task dependencies and parallel execution.

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

Every task must implement this interface:

interface Task {
taskId(): string; // Unique identifier
dependencies(): string[]; // Task IDs this depends on
}

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

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

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
`);

The shell emits events throughout execution, enabling rich logging and progress tracking:

EventDescriptionPayload
spawn:startCommand starting execution{ cmd: string, cwd: string }
spawn:doneCommand completed successfully{ cmd: string, code: number, stdout: string }
spawn:errorCommand failed or errored{ cmd: string, error: Error }
task:line:startEval line starting (multi-line mode){ line: string, index: number }
task:line:doneEval line completed{ line: string, success: boolean }
shebang:tempfileTemporary script file created{ path: string, shebang: string }
shebang:cleanupTemporary file removed{ path: string }
auto:modeExecution 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

cline.ts - Code Fence Command-Line Parsing

Section titled “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

Section titled “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.

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)

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.

lister-tabular-tui.ts - Tabular Terminal UI

Section titled “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

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.

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 --> lint

Use Cases:

  • Debugging complex task dependencies
  • Documenting build processes
  • Visualizing CI/CD pipelines

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.0

Essential for tools that depend on external commands - helps users troubleshoot missing dependencies.

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 once

Debouncing: Multiple rapid changes trigger only one rebuild after activity settles.

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.

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

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.

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.ts

Useful for displaying project structure, generating file lists, or visualizing changes.

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.

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 };
});

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);

Don’t hardcode console.log in your logic - emit events instead:

// ❌ Bad: Tight coupling
function runTask() {
console.log("Starting...");
// ...
console.log("Done!");
}
// ✅ Good: Use event bus
function runTask(bus: EventBus) {
bus.emit("start", { id: taskId });
// ...
bus.emit("done", { id: taskId });
}

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!

Don’t rebuild execution plans unnecessarily:

// ✅ Build once, reuse
const plan = executionPlan(tasks);
// Execute multiple times
await executeDAG(plan, handler1);
await executeDAG(plan, handler2);

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}`);
}

Prevent excessive rebuilds with appropriate debounce times:

// ✅ 100-500ms is usually good
const run = watcher(files, rebuild, { debounceMs: 200 });
  • 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 spawnText for simple commands, denoTaskEval for complex scripts
  • Event Buses: Minimal overhead, but avoid emitting events in hot loops
Terminal window
# 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 coverage
// 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);
});
// Watch and rebuild automatically
await watcher(
["src/**/*.ts", "Spryfile.md"],
async () => {
const plan = executionSubplan(fullPlan, ["build"]);
await executeDAG(plan, handler);
},
{ debounceMs: 150 }
);
// 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...

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.