import { Command } from 'commander';
import { writeFileSync, mkdirSync } from 'fs';
import { resolve, join, basename, extname } from 'path';
import { parseConfig } from './parser.js';
import { simulate } from './simulate.js';
import { generateSVG } from './exporters/svg.js';
import { generatePNG } from './exporters/png.js';
import { generateStandaloneHTML, generateEmbedJS } from './exporters/html.js';
const program = new Command();
program
.name('polymap')
.description('Generate interactive polycule relationship maps from YAML')
.version('0.1.0');
program
.command('generate ')
.description('Generate map from a YAML config file')
.option('-o, --output
', 'output directory', '.')
.option(
'-f, --format ',
'comma-separated output formats: html,svg,png',
'html,svg,png'
)
.option('-n, --name ', 'output filename prefix (default: input filename without extension)')
.option('--width ', 'PNG output width in pixels', '1400')
.option('--title ', 'HTML page title', 'Polycule Map')
.option('--legend', 'render relationship legend on SVG/PNG exports', false)
.option('--no-labels', 'hide edge label text on SVG/PNG exports')
.option('--no-names', 'hide node name labels on SVG/PNG exports')
.action(async (input: string, opts) => {
const inputPath = resolve(input);
const outputDir = resolve(opts.output as string);
const formats = (opts.format as string).split(',').map(s => s.trim().toLowerCase());
const prefix = (opts.name as string | undefined) ?? basename(inputPath, extname(inputPath));
const pngWidth = parseInt(opts.width as string, 10) || 1400;
const title = opts.title as string;
const showLegend = Boolean(opts.legend);
const showEdgeLabels = opts.labels !== false;
const showNames = opts.names !== false;
mkdirSync(outputDir, { recursive: true });
let data;
try {
data = parseConfig(inputPath);
} catch (err) {
console.error(`Error parsing config: ${(err as Error).message}`);
process.exit(1);
}
console.log(`Loaded ${data.people.length} people, ${data.relationships.length} relationships`);
// Run force simulation for static exports
const needsSimulation = formats.includes('svg') || formats.includes('png');
let simResult: ReturnType | null = null;
if (needsSimulation) {
process.stdout.write('Running force simulation...');
simResult = simulate(data);
process.stdout.write(' done\n');
}
for (const fmt of formats) {
switch (fmt) {
case 'html': {
const htmlPath = join(outputDir, `${prefix}.html`);
const embedPath = join(outputDir, `${prefix}-embed.js`);
writeFileSync(htmlPath, generateStandaloneHTML(data, { title }), 'utf8');
writeFileSync(embedPath, generateEmbedJS(data), 'utf8');
console.log(`HTML → ${htmlPath}`);
console.log(`Embed → ${embedPath}`);
break;
}
case 'svg': {
const svgPath = join(outputDir, `${prefix}.svg`);
const svgStr = await generateSVG(data, simResult!.nodes, simResult!.links, { showLegend, showEdgeLabels, showNames });
writeFileSync(svgPath, svgStr, 'utf8');
console.log(`SVG → ${svgPath}`);
break;
}
case 'png': {
const pngPath = join(outputDir, `${prefix}.png`);
const pngBuf = await generatePNG(data, simResult!.nodes, simResult!.links, pngWidth, showLegend, showEdgeLabels, showNames);
writeFileSync(pngPath, pngBuf);
console.log(`PNG → ${pngPath}`);
break;
}
default:
console.warn(`Unknown format "${fmt}" — skipping`);
}
}
console.log('Done.');
});
program.parseAsync(process.argv).catch(err => {
console.error(err);
process.exit(1);
});