render-graphs.js 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169
  1. #!/usr/bin/env node
  2. /**
  3. * Render graphviz diagrams from a skill's SKILL.md to SVG files.
  4. *
  5. * Usage:
  6. * ./render-graphs.js <skill-directory> # Render each diagram separately
  7. * ./render-graphs.js <skill-directory> --combine # Combine all into one diagram
  8. *
  9. * Extracts all ```dot blocks from SKILL.md and renders to SVG.
  10. * Useful for helping your human partner visualize the process flows.
  11. *
  12. * Requires: graphviz (dot) installed on system
  13. */
  14. import * as fs from 'fs';
  15. import * as path from 'path';
  16. import { execFileSync } from 'child_process';
  17. function extractDotBlocks(markdown) {
  18. const blocks = [];
  19. const regex = /```dot\n([\s\S]*?)```/g;
  20. let match;
  21. while ((match = regex.exec(markdown)) !== null) {
  22. const content = match[1].trim();
  23. // Extract digraph name
  24. const nameMatch = content.match(/digraph\s+(\w+)/);
  25. const name = nameMatch ? nameMatch[1] : `graph_${blocks.length + 1}`;
  26. blocks.push({ name, content });
  27. }
  28. return blocks;
  29. }
  30. function extractGraphBody(dotContent) {
  31. // Extract just the body (nodes and edges) from a digraph
  32. const match = dotContent.match(/digraph\s+\w+\s*\{([\s\S]*)\}/);
  33. if (!match) return '';
  34. let body = match[1];
  35. // Remove rankdir (we'll set it once at the top level)
  36. body = body.replace(/^\s*rankdir\s*=\s*\w+\s*;?\s*$/gm, '');
  37. return body.trim();
  38. }
  39. function combineGraphs(blocks, skillName) {
  40. const bodies = blocks.map((block, i) => {
  41. const body = extractGraphBody(block.content);
  42. // Wrap each subgraph in a cluster for visual grouping
  43. return ` subgraph cluster_${i} {
  44. label="${block.name}";
  45. ${body.split('\n').map(line => ' ' + line).join('\n')}
  46. }`;
  47. });
  48. return `digraph ${skillName}_combined {
  49. rankdir=TB;
  50. compound=true;
  51. newrank=true;
  52. ${bodies.join('\n\n')}
  53. }`;
  54. }
  55. function renderToSvg(dotContent) {
  56. try {
  57. return execFileSync('dot', ['-Tsvg'], {
  58. input: dotContent,
  59. encoding: 'utf-8',
  60. maxBuffer: 10 * 1024 * 1024
  61. });
  62. } catch (err) {
  63. console.error('Error running dot:', err.message);
  64. if (err.stderr) console.error(err.stderr.toString());
  65. return null;
  66. }
  67. }
  68. function main() {
  69. const args = process.argv.slice(2);
  70. const combine = args.includes('--combine');
  71. const skillDirArg = args.find(a => !a.startsWith('--'));
  72. if (!skillDirArg) {
  73. console.error('Usage: render-graphs.js <skill-directory> [--combine]');
  74. console.error('');
  75. console.error('Options:');
  76. console.error(' --combine Combine all diagrams into one SVG');
  77. console.error('');
  78. console.error('Example:');
  79. console.error(' ./render-graphs.js ../subagent-driven-development');
  80. console.error(' ./render-graphs.js ../subagent-driven-development --combine');
  81. process.exit(1);
  82. }
  83. const skillDir = path.resolve(skillDirArg);
  84. const skillFile = path.join(skillDir, 'SKILL.md');
  85. const skillName = path.basename(skillDir).replace(/-/g, '_');
  86. if (!fs.existsSync(skillFile)) {
  87. console.error(`Error: ${skillFile} not found`);
  88. process.exit(1);
  89. }
  90. // Check if dot is available. Run the binary directly rather than probing
  91. // with `which`, which is not a command on Windows.
  92. try {
  93. execFileSync('dot', ['-V'], { stdio: 'ignore' });
  94. } catch {
  95. console.error('Error: graphviz (dot) not found. Install with:');
  96. console.error(' brew install graphviz # macOS');
  97. console.error(' apt install graphviz # Linux');
  98. process.exit(1);
  99. }
  100. const markdown = fs.readFileSync(skillFile, 'utf-8');
  101. const blocks = extractDotBlocks(markdown);
  102. if (blocks.length === 0) {
  103. console.log('No ```dot blocks found in', skillFile);
  104. process.exit(0);
  105. }
  106. console.log(`Found ${blocks.length} diagram(s) in ${path.basename(skillDir)}/SKILL.md`);
  107. const outputDir = path.join(skillDir, 'diagrams');
  108. if (!fs.existsSync(outputDir)) {
  109. fs.mkdirSync(outputDir);
  110. }
  111. if (combine) {
  112. // Combine all graphs into one
  113. const combined = combineGraphs(blocks, skillName);
  114. const svg = renderToSvg(combined);
  115. if (svg) {
  116. const outputPath = path.join(outputDir, `${skillName}_combined.svg`);
  117. fs.writeFileSync(outputPath, svg);
  118. console.log(` Rendered: ${skillName}_combined.svg`);
  119. // Also write the dot source for debugging
  120. const dotPath = path.join(outputDir, `${skillName}_combined.dot`);
  121. fs.writeFileSync(dotPath, combined);
  122. console.log(` Source: ${skillName}_combined.dot`);
  123. } else {
  124. console.error(' Failed to render combined diagram');
  125. }
  126. } else {
  127. // Render each separately
  128. for (const block of blocks) {
  129. const svg = renderToSvg(block.content);
  130. if (svg) {
  131. const outputPath = path.join(outputDir, `${block.name}.svg`);
  132. fs.writeFileSync(outputPath, svg);
  133. console.log(` Rendered: ${block.name}.svg`);
  134. } else {
  135. console.error(` Failed: ${block.name}`);
  136. }
  137. }
  138. }
  139. console.log(`\nOutput: ${outputDir}/`);
  140. }
  141. main();