Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
Creates and validates agent skills using Test-Driven Development — write test scenarios, baseline behavior, then the skill itself.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
render-graphs.js
1#!/usr/bin/env node23/**4* Render graphviz diagrams from a skill's SKILL.md to SVG files.5*6* Usage:7* ./render-graphs.js <skill-directory> # Render each diagram separately8* ./render-graphs.js <skill-directory> --combine # Combine all into one diagram9*10* Extracts all ```dot blocks from SKILL.md and renders to SVG.11* Useful for helping your human partner visualize the process flows.12*13* Requires: graphviz (dot) installed on system14*/1516const fs = require('fs');17const path = require('path');18const { execSync } = require('child_process');1920function extractDotBlocks(markdown) {21const blocks = [];22const regex = /```dot\n([\s\S]*?)```/g;23let match;2425while ((match = regex.exec(markdown)) !== null) {26const content = match[1].trim();2728// Extract digraph name29const nameMatch = content.match(/digraph\s+(\w+)/);30const name = nameMatch ? nameMatch[1] : `graph_${blocks.length + 1}`;3132blocks.push({ name, content });33}3435return blocks;36}3738function extractGraphBody(dotContent) {39// Extract just the body (nodes and edges) from a digraph40const match = dotContent.match(/digraph\s+\w+\s*\{([\s\S]*)\}/);41if (!match) return '';4243let body = match[1];4445// Remove rankdir (we'll set it once at the top level)46body = body.replace(/^\s*rankdir\s*=\s*\w+\s*;?\s*$/gm, '');4748return body.trim();49}5051function combineGraphs(blocks, skillName) {52const bodies = blocks.map((block, i) => {53const body = extractGraphBody(block.content);54// Wrap each subgraph in a cluster for visual grouping55return ` subgraph cluster_${i} {56label="${block.name}";57${body.split('\n').map(line => ' ' + line).join('\n')}58}`;59});6061return `digraph ${skillName}_combined {62rankdir=TB;63compound=true;64newrank=true;6566${bodies.join('\n\n')}67}`;68}6970function renderToSvg(dotContent) {71try {72return execSync('dot -Tsvg', {73input: dotContent,74encoding: 'utf-8',75maxBuffer: 10 * 1024 * 102476});77} catch (err) {78console.error('Error running dot:', err.message);79if (err.stderr) console.error(err.stderr.toString());80return null;81}82}8384function main() {85const args = process.argv.slice(2);86const combine = args.includes('--combine');87const skillDirArg = args.find(a => !a.startsWith('--'));8889if (!skillDirArg) {90console.error('Usage: render-graphs.js <skill-directory> [--combine]');91console.error('');92console.error('Options:');93console.error(' --combine Combine all diagrams into one SVG');94console.error('');95console.error('Example:');96console.error(' ./render-graphs.js ../subagent-driven-development');97console.error(' ./render-graphs.js ../subagent-driven-development --combine');98process.exit(1);99}100101const skillDir = path.resolve(skillDirArg);102const skillFile = path.join(skillDir, 'SKILL.md');103const skillName = path.basename(skillDir).replace(/-/g, '_');104105if (!fs.existsSync(skillFile)) {106console.error(`Error: ${skillFile} not found`);107process.exit(1);108}109110// Check if dot is available111try {112execSync('which dot', { encoding: 'utf-8' });113} catch {114console.error('Error: graphviz (dot) not found. Install with:');115console.error(' brew install graphviz # macOS');116console.error(' apt install graphviz # Linux');117process.exit(1);118}119120const markdown = fs.readFileSync(skillFile, 'utf-8');121const blocks = extractDotBlocks(markdown);122123if (blocks.length === 0) {124console.log('No ```dot blocks found in', skillFile);125process.exit(0);126}127128console.log(`Found ${blocks.length} diagram(s) in ${path.basename(skillDir)}/SKILL.md`);129130const outputDir = path.join(skillDir, 'diagrams');131if (!fs.existsSync(outputDir)) {132fs.mkdirSync(outputDir);133}134135if (combine) {136// Combine all graphs into one137const combined = combineGraphs(blocks, skillName);138const svg = renderToSvg(combined);139if (svg) {140const outputPath = path.join(outputDir, `${skillName}_combined.svg`);141fs.writeFileSync(outputPath, svg);142console.log(` Rendered: ${skillName}_combined.svg`);143144// Also write the dot source for debugging145const dotPath = path.join(outputDir, `${skillName}_combined.dot`);146fs.writeFileSync(dotPath, combined);147console.log(` Source: ${skillName}_combined.dot`);148} else {149console.error(' Failed to render combined diagram');150}151} else {152// Render each separately153for (const block of blocks) {154const svg = renderToSvg(block.content);155if (svg) {156const outputPath = path.join(outputDir, `${block.name}.svg`);157fs.writeFileSync(outputPath, svg);158console.log(` Rendered: ${block.name}.svg`);159} else {160console.error(` Failed: ${block.name}`);161}162}163}164165console.log(`\nOutput: ${outputDir}/`);166}167168main();169