ソースを参照

Add render-graphs.js tool for visualizing skill flowcharts

Jesse Vincent 9 ヶ月 前
コミット
28ba020000
2 ファイル変更174 行追加0 行削除
  1. 6 0
      skills/writing-skills/SKILL.md
  2. 168 0
      skills/writing-skills/render-graphs.js

+ 6 - 0
skills/writing-skills/SKILL.md

@@ -288,6 +288,12 @@ digraph when_flowchart {
 
 See @graphviz-conventions.dot for graphviz style rules.
 
+**Visualizing for your human partner:** Use `render-graphs.js` in this directory to render a skill's flowcharts to SVG:
+```bash
+./render-graphs.js ../some-skill           # Each diagram separately
+./render-graphs.js ../some-skill --combine # All diagrams in one SVG
+```
+
 ## Code Examples
 
 **One excellent example beats many mediocre ones**

+ 168 - 0
skills/writing-skills/render-graphs.js

@@ -0,0 +1,168 @@
+#!/usr/bin/env node
+
+/**
+ * Render graphviz diagrams from a skill's SKILL.md to SVG files.
+ *
+ * Usage:
+ *   ./render-graphs.js <skill-directory>           # Render each diagram separately
+ *   ./render-graphs.js <skill-directory> --combine # Combine all into one diagram
+ *
+ * Extracts all ```dot blocks from SKILL.md and renders to SVG.
+ * Useful for helping your human partner visualize the process flows.
+ *
+ * Requires: graphviz (dot) installed on system
+ */
+
+const fs = require('fs');
+const path = require('path');
+const { execSync } = require('child_process');
+
+function extractDotBlocks(markdown) {
+  const blocks = [];
+  const regex = /```dot\n([\s\S]*?)```/g;
+  let match;
+
+  while ((match = regex.exec(markdown)) !== null) {
+    const content = match[1].trim();
+
+    // Extract digraph name
+    const nameMatch = content.match(/digraph\s+(\w+)/);
+    const name = nameMatch ? nameMatch[1] : `graph_${blocks.length + 1}`;
+
+    blocks.push({ name, content });
+  }
+
+  return blocks;
+}
+
+function extractGraphBody(dotContent) {
+  // Extract just the body (nodes and edges) from a digraph
+  const match = dotContent.match(/digraph\s+\w+\s*\{([\s\S]*)\}/);
+  if (!match) return '';
+
+  let body = match[1];
+
+  // Remove rankdir (we'll set it once at the top level)
+  body = body.replace(/^\s*rankdir\s*=\s*\w+\s*;?\s*$/gm, '');
+
+  return body.trim();
+}
+
+function combineGraphs(blocks, skillName) {
+  const bodies = blocks.map((block, i) => {
+    const body = extractGraphBody(block.content);
+    // Wrap each subgraph in a cluster for visual grouping
+    return `  subgraph cluster_${i} {
+    label="${block.name}";
+    ${body.split('\n').map(line => '  ' + line).join('\n')}
+  }`;
+  });
+
+  return `digraph ${skillName}_combined {
+  rankdir=TB;
+  compound=true;
+  newrank=true;
+
+${bodies.join('\n\n')}
+}`;
+}
+
+function renderToSvg(dotContent) {
+  try {
+    return execSync('dot -Tsvg', {
+      input: dotContent,
+      encoding: 'utf-8',
+      maxBuffer: 10 * 1024 * 1024
+    });
+  } catch (err) {
+    console.error('Error running dot:', err.message);
+    if (err.stderr) console.error(err.stderr.toString());
+    return null;
+  }
+}
+
+function main() {
+  const args = process.argv.slice(2);
+  const combine = args.includes('--combine');
+  const skillDirArg = args.find(a => !a.startsWith('--'));
+
+  if (!skillDirArg) {
+    console.error('Usage: render-graphs.js <skill-directory> [--combine]');
+    console.error('');
+    console.error('Options:');
+    console.error('  --combine    Combine all diagrams into one SVG');
+    console.error('');
+    console.error('Example:');
+    console.error('  ./render-graphs.js ../subagent-driven-development');
+    console.error('  ./render-graphs.js ../subagent-driven-development --combine');
+    process.exit(1);
+  }
+
+  const skillDir = path.resolve(skillDirArg);
+  const skillFile = path.join(skillDir, 'SKILL.md');
+  const skillName = path.basename(skillDir).replace(/-/g, '_');
+
+  if (!fs.existsSync(skillFile)) {
+    console.error(`Error: ${skillFile} not found`);
+    process.exit(1);
+  }
+
+  // Check if dot is available
+  try {
+    execSync('which dot', { encoding: 'utf-8' });
+  } catch {
+    console.error('Error: graphviz (dot) not found. Install with:');
+    console.error('  brew install graphviz    # macOS');
+    console.error('  apt install graphviz     # Linux');
+    process.exit(1);
+  }
+
+  const markdown = fs.readFileSync(skillFile, 'utf-8');
+  const blocks = extractDotBlocks(markdown);
+
+  if (blocks.length === 0) {
+    console.log('No ```dot blocks found in', skillFile);
+    process.exit(0);
+  }
+
+  console.log(`Found ${blocks.length} diagram(s) in ${path.basename(skillDir)}/SKILL.md`);
+
+  const outputDir = path.join(skillDir, 'diagrams');
+  if (!fs.existsSync(outputDir)) {
+    fs.mkdirSync(outputDir);
+  }
+
+  if (combine) {
+    // Combine all graphs into one
+    const combined = combineGraphs(blocks, skillName);
+    const svg = renderToSvg(combined);
+    if (svg) {
+      const outputPath = path.join(outputDir, `${skillName}_combined.svg`);
+      fs.writeFileSync(outputPath, svg);
+      console.log(`  Rendered: ${skillName}_combined.svg`);
+
+      // Also write the dot source for debugging
+      const dotPath = path.join(outputDir, `${skillName}_combined.dot`);
+      fs.writeFileSync(dotPath, combined);
+      console.log(`  Source: ${skillName}_combined.dot`);
+    } else {
+      console.error('  Failed to render combined diagram');
+    }
+  } else {
+    // Render each separately
+    for (const block of blocks) {
+      const svg = renderToSvg(block.content);
+      if (svg) {
+        const outputPath = path.join(outputDir, `${block.name}.svg`);
+        fs.writeFileSync(outputPath, svg);
+        console.log(`  Rendered: ${block.name}.svg`);
+      } else {
+        console.error(`  Failed: ${block.name}`);
+      }
+    }
+  }
+
+  console.log(`\nOutput: ${outputDir}/`);
+}
+
+main();