Spry LogoSpry Docs

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 path

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

run-spryfile.ts
#!/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

On this page