How do I have so many partners??
1import { Command } from 'commander';
2import { writeFileSync, mkdirSync } from 'fs';
3import { resolve, join, basename, extname } from 'path';
4import { parseConfig } from './parser.js';
5import { simulate } from './simulate.js';
6import { generateSVG } from './exporters/svg.js';
7import { generatePNG } from './exporters/png.js';
8import { generateStandaloneHTML, generateEmbedJS } from './exporters/html.js';
9
10const program = new Command();
11
12program
13 .name('polymap')
14 .description('Generate interactive polycule relationship maps from YAML')
15 .version('0.1.0');
16
17program
18 .command('generate <input>')
19 .description('Generate map from a YAML config file')
20 .option('-o, --output <dir>', 'output directory', '.')
21 .option(
22 '-f, --format <formats>',
23 'comma-separated output formats: html,svg,png',
24 'html,svg,png'
25 )
26 .option('-n, --name <prefix>', 'output filename prefix (default: input filename without extension)')
27 .option('--width <pixels>', 'PNG output width in pixels', '1400')
28 .option('--title <title>', 'HTML page title', 'Polycule Map')
29 .option('--legend', 'render relationship legend on SVG/PNG exports', false)
30 .option('--no-labels', 'hide edge label text on SVG/PNG exports')
31 .option('--no-names', 'hide node name labels on SVG/PNG exports')
32 .action(async (input: string, opts) => {
33 const inputPath = resolve(input);
34 const outputDir = resolve(opts.output as string);
35 const formats = (opts.format as string).split(',').map(s => s.trim().toLowerCase());
36 const prefix = (opts.name as string | undefined) ?? basename(inputPath, extname(inputPath));
37 const pngWidth = parseInt(opts.width as string, 10) || 1400;
38 const title = opts.title as string;
39 const showLegend = Boolean(opts.legend);
40 const showEdgeLabels = opts.labels !== false;
41 const showNames = opts.names !== false;
42
43 mkdirSync(outputDir, { recursive: true });
44
45 let data;
46 try {
47 data = parseConfig(inputPath);
48 } catch (err) {
49 console.error(`Error parsing config: ${(err as Error).message}`);
50 process.exit(1);
51 }
52
53 console.log(`Loaded ${data.people.length} people, ${data.relationships.length} relationships`);
54
55 // Run force simulation for static exports
56 const needsSimulation = formats.includes('svg') || formats.includes('png');
57 let simResult: ReturnType<typeof simulate> | null = null;
58 if (needsSimulation) {
59 process.stdout.write('Running force simulation...');
60 simResult = simulate(data);
61 process.stdout.write(' done\n');
62 }
63
64 for (const fmt of formats) {
65 switch (fmt) {
66 case 'html': {
67 const htmlPath = join(outputDir, `${prefix}.html`);
68 const embedPath = join(outputDir, `${prefix}-embed.js`);
69 writeFileSync(htmlPath, generateStandaloneHTML(data, { title }), 'utf8');
70 writeFileSync(embedPath, generateEmbedJS(data), 'utf8');
71 console.log(`HTML → ${htmlPath}`);
72 console.log(`Embed → ${embedPath}`);
73 break;
74 }
75
76 case 'svg': {
77 const svgPath = join(outputDir, `${prefix}.svg`);
78 const svgStr = await generateSVG(data, simResult!.nodes, simResult!.links, { showLegend, showEdgeLabels, showNames });
79 writeFileSync(svgPath, svgStr, 'utf8');
80 console.log(`SVG → ${svgPath}`);
81 break;
82 }
83
84 case 'png': {
85 const pngPath = join(outputDir, `${prefix}.png`);
86 const pngBuf = await generatePNG(data, simResult!.nodes, simResult!.links, pngWidth, showLegend, showEdgeLabels, showNames);
87 writeFileSync(pngPath, pngBuf);
88 console.log(`PNG → ${pngPath}`);
89 break;
90 }
91
92 default:
93 console.warn(`Unknown format "${fmt}" — skipping`);
94 }
95 }
96
97 console.log('Done.');
98 });
99
100program.parseAsync(process.argv).catch(err => {
101 console.error(err);
102 process.exit(1);
103});