API Reference
Use Spry's libraries directly in TypeScript code
Overview
Spry's functionality is available as modular TypeScript libraries that can be imported and used programmatically. This enables:
- Building custom tools
- Integrating Spry into larger applications
- Creating specialized processing pipelines
- Automated testing of Spryfiles
Installation
Import from the Spry repository:
// Deno
import { markdownASTs } from "https://raw.githubusercontent.com/programmablemd/spry/main/lib/axiom/io/mod.ts";
import { graph, typicalRules } from "https://raw.githubusercontent.com/programmablemd/spry/main/lib/axiom/graph.ts";// Clone and import locally
import { markdownASTs } from "./lib/axiom/io/mod.ts";
import { graph, typicalRules } from "./lib/axiom/graph.ts";Core Modules
Loading Markdown
Load and parse Markdown files into AST representations:
import { markdownASTs } from "@spry/axiom/io";
// Load from file
const [doc] = await markdownASTs({
sources: ["./runbook.md"],
cwd: Deno.cwd(),
});
// Load from multiple sources
const docs = await markdownASTs({
sources: ["./docs/*.md", "https://example.com/doc.md"],
cwd: Deno.cwd(),
});
// Access the AST
console.log(doc.root.type); // "root"
console.log(doc.vfile.path); // File pathBuilding Graphs
Build semantic graphs from parsed ASTs:
import { graph, typicalRules } from "@spry/axiom/graph";
// Build semantic graph
const g = graph(doc.root, typicalRules());
// Access relationships
for (const edge of g.edges) {
console.log(`${edge.rel}: ${edge.from.type} -> ${edge.to.type}`);
}Creating Projections
Transform graphs into domain-specific models:
import { buildFlexibleProjection } from "@spry/axiom/projection/flexible";
import { buildPlaybookProjection } from "@spry/axiom/projection/playbook";
// UI-neutral projection
const flex = buildFlexibleProjection(g);
console.log("Documents:", flex.documents.length);
console.log("Nodes:", flex.nodes.length);
console.log("Edges:", flex.edges.length);
// Execution-focused projection
const playbook = buildPlaybookProjection(g);
console.log("Executables:", playbook.executables.length);
console.log("Tasks:", playbook.tasks.length);Executing Tasks
Execute tasks in dependency order:
import { executionPlan, executeDAG } from "@spry/axiom/orchestrate/task";
import { tasksRunbook } from "@spry/axiom/orchestrate/runbook";
// Build execution plan from tasks
const plan = executionPlan(playbook.tasks);
// View execution order
console.log("Execution order:");
for (const layer of plan.layers) {
console.log(" Layer:", layer.map(t => t.taskId()));
}
// Execute tasks
const runbook = tasksRunbook(playbook.tasks, {
interpolationContext: { env: Deno.env.toObject() },
});
await executeDAG(plan, {
executor: runbook.executor,
onTaskStart: (task) => console.log(`Starting: ${task.taskId()}`),
onTaskComplete: (task, result) => {
console.log(`Completed: ${task.taskId()} (${result.exitCode})`);
},
onTaskFailed: (task, error) => {
console.error(`Failed: ${task.taskId()}`, error);
},
});Common Patterns
Parse and Analyze
Analyze a Spryfile and extract task information:
import { markdownASTs } from "@spry/axiom/io";
import { graph, typicalRules } from "@spry/axiom/graph";
import { buildPlaybookProjection } from "@spry/axiom/projection/playbook";
async function analyzeSpryfile(path: string) {
const [doc] = await markdownASTs({ sources: [path] });
const g = graph(doc.root, typicalRules());
const playbook = buildPlaybookProjection(g);
return {
taskCount: playbook.tasks.length,
tasks: playbook.tasks.map(t => ({
id: t.taskId(),
deps: t.taskDeps(),
description: t.spawnableArgs?.descr,
})),
};
}
const analysis = await analyzeSpryfile("./runbook.md");
console.log(JSON.stringify(analysis, null, 2));Custom Validation
Implement custom validation logic for Spryfiles:
import { visit } from "unist-util-visit";
import type { Code } from "mdast";
interface ValidationResult {
valid: boolean;
errors: string[];
}
function validateSpryfile(root: Root): ValidationResult {
const errors: string[] = [];
const taskIds = new Set<string>();
visit<Root, "code">(root, "code", (node: Code) => {
const fm = node.data?.codeFM;
if (!fm?.identity) return;
// Check for duplicates
if (taskIds.has(fm.identity)) {
errors.push(`Duplicate task ID: ${fm.identity}`);
}
taskIds.add(fm.identity);
// Check for missing descriptions
if (!fm.spawnableArgs?.descr) {
errors.push(`Task "${fm.identity}" missing description`);
}
// Check dependencies exist
for (const dep of fm.spawnableArgs?.dep || []) {
if (!taskIds.has(dep) && dep !== fm.identity) {
// Note: This simple check doesn't account for order
// A real implementation would do a second pass
}
}
});
return {
valid: errors.length === 0,
errors,
};
}Generate Reports
Generate detailed reports about tasks:
import { toString } from "mdast-util-to-string";
interface TaskReport {
id: string;
description: string;
dependencies: string[];
section: string;
lineCount: number;
}
function generateTaskReport(playbook: PlaybookProjection, graph: Graph): TaskReport[] {
return playbook.tasks.map(task => {
// Find containing section
const sectionEdge = graph.edges.find(
e => e.from === task.origin && e.rel === "containedInSection"
);
const section = sectionEdge ? toString(sectionEdge.to) : "Unknown";
return {
id: task.taskId(),
description: task.spawnableArgs?.descr || "",
dependencies: task.taskDeps() || [],
section,
lineCount: task.origin.value.split("\n").length,
};
});
}Watch and Rebuild
Watch for file changes and rebuild automatically:
import { markdownASTs } from "@spry/axiom/io";
async function watchSpryfile(path: string, onChange: (doc: any) => void) {
const watcher = Deno.watchFs(path);
// Initial load
const [doc] = await markdownASTs({ sources: [path] });
onChange(doc);
// Watch for changes
for await (const event of watcher) {
if (event.kind === "modify") {
const [doc] = await markdownASTs({ sources: [path] });
onChange(doc);
}
}
}
// Usage
watchSpryfile("./runbook.md", (doc) => {
console.log("Document updated!");
// Rebuild, validate, etc.
});Shell Execution
Execute shell commands programmatically:
import { shell, spawn } from "@spry/universal/shell";
// Simple command execution
const result = await shell.bash("echo 'Hello, World!'");
console.log(result.stdout); // "Hello, World!\n"
// Spawn with options
const { exitCode, stdout, stderr } = await spawn("npm", ["test"], {
cwd: "./project",
env: { NODE_ENV: "test" },
timeout: 60000,
});Template Rendering
Render templates with interpolation:
import { render, createContext } from "@spry/universal/render";
// Create interpolation context
const context = createContext({
env: Deno.env.toObject(),
config: { version: "1.0.0" },
memory: new Map(),
});
// Render template
const template = "Deploying version ${config.version} to ${env.DEPLOY_ENV}";
const result = await render(template, context);
console.log(result); // "Deploying version 1.0.0 to production"Language Registry
Work with programming language metadata:
import { languageRegistry, detectLanguage } from "@spry/universal/code";
// Get language info
const bash = languageRegistry.get("bash");
console.log(bash?.extensions); // [".sh", ".bash"]
console.log(bash?.comment); // { line: "#" }
// Detect language from filename
const lang = detectLanguage("script.py");
console.log(lang?.id); // "python"DAG Utilities
Build and manage directed acyclic graphs:
import { executionPlan, topologicalSort } from "@spry/universal/task";
// Create tasks
const tasks = [
{ taskId: () => "a", taskDeps: () => [] },
{ taskId: () => "b", taskDeps: () => ["a"] },
{ taskId: () => "c", taskDeps: () => ["a"] },
{ taskId: () => "d", taskDeps: () => ["b", "c"] },
];
// Build execution plan
const plan = executionPlan(tasks);
console.log("Layers:");
for (const layer of plan.layers) {
console.log(" ", layer.map(t => t.taskId()));
}
// Layers:
// ["a"]
// ["b", "c"]
// ["d"]Complete Example
This example demonstrates a complete Spry runner that loads, analyzes, and executes a Spryfile.
#!/usr/bin/env -S deno run -A
import { markdownASTs } from "./lib/axiom/io/mod.ts";
import { graph, typicalRules } from "./lib/axiom/graph.ts";
import { buildPlaybookProjection } from "./lib/axiom/projection/playbook.ts";
import { executionPlan, executeDAG } from "./lib/axiom/orchestrate/task.ts";
import { tasksRunbook } from "./lib/axiom/orchestrate/runbook.ts";
async function runSpryfile(path: string) {
console.log(`Loading: ${path}`);
// 1. Load and parse
const [doc] = await markdownASTs({
sources: [path],
cwd: Deno.cwd(),
});
// 2. Build semantic graph
const g = graph(doc.root, typicalRules());
// 3. Create playbook projection
const playbook = buildPlaybookProjection(g);
console.log(`Found ${playbook.tasks.length} tasks`);
// 4. Build execution plan
const plan = executionPlan(playbook.tasks);
// 5. Create runbook executor
const runbook = tasksRunbook(playbook.tasks, {
interpolationContext: {
env: Deno.env.toObject(),
},
});
// 6. Execute
console.log("\nExecuting tasks...\n");
await executeDAG(plan, {
executor: runbook.executor,
onTaskStart: (task) => {
console.log(`▶ ${task.taskId()}`);
},
onTaskComplete: (task, result) => {
if (result.stdout) {
console.log(result.stdout);
}
console.log(`✓ ${task.taskId()} (exit: ${result.exitCode})\n`);
},
onTaskFailed: (task, error) => {
console.error(`✗ ${task.taskId()} failed:`, error);
},
});
console.log("Done!");
}
// Run
const file = Deno.args[0] || "runbook.md";
await runSpryfile(file);See Also
How is this guide?
Last updated on