Setting the file. One moment.
Render Graphs · Writing Skills · obra/superpowers · Skills Docs
ContentsBack to the top of the page render-graphs.js
JavaScript · 168 lines · 5 KB
fs
=
require
(
'fs'
);
17 const path = require ( 'path' );
18 const { execSync } = require ( 'child_process' );
19
20 function extractDotBlocks ( markdown ) {
21 const blocks = [];
22 const regex = /```dot \n ( [\s\S] *? )```/ g ;
23 let match;
24
25 while ((match = regex. exec (markdown)) !== null ) {
26 const content = match[ 1 ]. trim ();
27
28 // Extract digraph name
29 const nameMatch = content. match ( /digraph \s + ( \w + )/ );
30 const name = nameMatch ? nameMatch[ 1 ] : `graph_${ blocks . length + 1 }` ;
31
32 blocks. push ({ name, content });
33 }
34
35 return blocks;
36 }
37
38 function extractGraphBody ( dotContent ) {
39 // Extract just the body (nodes and edges) from a digraph
40 const match = dotContent. match ( /digraph \s + \w + \s * \{ ( [\s\S] * ) \} / );
41 if ( ! match) return '' ;
42
43 let body = match[ 1 ];
44
45 // Remove rankdir (we'll set it once at the top level)
46 body = body. replace ( / ^ \s * rankdir \s * = \s * \w + \s * ; ? \s *$ / gm , '' );
47
48 return body. trim ();
49 }
50
51 function combineGraphs ( blocks , skillName ) {
52 const bodies = blocks. map (( block , i ) => {
53 const body = extractGraphBody (block.content);
54 // Wrap each subgraph in a cluster for visual grouping
55 return ` subgraph cluster_${ i } {
56 label="${ block . name }";
57 ${ body . split ( ' \n ' ). map ( line => ' ' + line ). join ( ' \n ' ) }
58 }` ;
59 });
60
61 return `digraph ${ skillName }_combined {
62 rankdir=TB;
63 compound=true;
64 newrank=true;
65
66 ${ bodies . join ( ' \n\n ' ) }
67 }` ;
68 }
69
70 function renderToSvg ( dotContent ) {
71 try {
72 return execSync ( 'dot -Tsvg' , {
73 input: dotContent,
74 encoding: 'utf-8' ,
75 maxBuffer: 10 * 1024 * 1024
76 });
77 } catch (err) {
78 console. error ( 'Error running dot:' , err.message);
79 if (err.stderr) console. error (err.stderr. toString ());
80 return null ;
81 }
82 }
83
84 function main () {
85 const args = process.argv. slice ( 2 );
86 const combine = args. includes ( '--combine' );
87 const skillDirArg = args. find ( a => ! a. startsWith ( '--' ));
88
89 if ( ! skillDirArg) {
90 console. error ( 'Usage: render-graphs.js <skill-directory> [--combine]' );
91 console. error ( '' );
92 console. error ( 'Options:' );
93 console. error ( ' --combine Combine all diagrams into one SVG' );
94 console. error ( '' );
95 console. error ( 'Example:' );
96 console. error ( ' ./render-graphs.js ../subagent-driven-development' );
97 console. error ( ' ./render-graphs.js ../subagent-driven-development --combine' );
98 process. exit ( 1 );
99 }
100
101 const skillDir = path. resolve (skillDirArg);
102 const skillFile = path. join (skillDir, 'SKILL.md' );
103 const skillName = path. basename (skillDir). replace ( /-/ g , '_' );
104
105 if ( ! fs. existsSync (skillFile)) {
106 console. error ( `Error: ${ skillFile } not found` );
107 process. exit ( 1 );
108 }
109
110 // Check if dot is available
111 try {
112 execSync ( 'which dot' , { encoding: 'utf-8' });
113 } catch {
114 console. error ( 'Error: graphviz (dot) not found. Install with:' );
115 console. error ( ' brew install graphviz # macOS' );
116 console. error ( ' apt install graphviz # Linux' );
117 process. exit ( 1 );
118 }
119
120 const markdown = fs. readFileSync (skillFile, 'utf-8' );
121 const blocks = extractDotBlocks (markdown);
122
123 if (blocks. length === 0 ) {
124 console. log ( 'No ```dot blocks found in' , skillFile);
125 process. exit ( 0 );
126 }
127
128 console. log ( `Found ${ blocks . length } diagram(s) in ${ path . basename ( skillDir ) }/SKILL.md` );
129
130 const outputDir = path. join (skillDir, 'diagrams' );
131 if ( ! fs. existsSync (outputDir)) {
132 fs. mkdirSync (outputDir);
133 }
134
135 if (combine) {
136 // Combine all graphs into one
137 const combined = combineGraphs (blocks, skillName);
138 const svg = renderToSvg (combined);
139 if (svg) {
140 const outputPath = path. join (outputDir, `${ skillName }_combined.svg` );
141 fs. writeFileSync (outputPath, svg);
142 console. log ( ` Rendered: ${ skillName }_combined.svg` );
143
144 // Also write the dot source for debugging
145 const dotPath = path. join (outputDir, `${ skillName }_combined.dot` );
146 fs. writeFileSync (dotPath, combined);
147 console. log ( ` Source: ${ skillName }_combined.dot` );
148 } else {
149 console. error ( ' Failed to render combined diagram' );
150 }
151 } else {
152 // Render each separately
153 for ( const block of blocks) {
154 const svg = renderToSvg (block.content);
155 if (svg) {
156 const outputPath = path. join (outputDir, `${ block . name }.svg` );
157 fs. writeFileSync (outputPath, svg);
158 console. log ( ` Rendered: ${ block . name }.svg` );
159 } else {
160 console. error ( ` Failed: ${ block . name }` );
161 }
162 }
163 }
164
165 console. log ( ` \n Output: ${ outputDir }/` );
166 }
167
168 main ();