Files
paperclip/server/src/services/company-export-readme.ts
Dotta 517e90c13a refactor: replace SVG org chart with Mermaid diagram in exports
- Org chart now uses a Mermaid flowchart (graph TD) instead of a
  standalone SVG file — GitHub and the preview both render it natively
- Removed SVG generation code, layout algorithm, and image resolution
- Removed images/org-chart.svg from export output
- Simplified ExportPreviewPane (no more SVG/data-URI handling)
- Both server and client README generators produce Mermaid diagrams

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-17 09:09:37 -05:00

147 lines
4.2 KiB
TypeScript

/**
* Generates README.md with Mermaid org chart for company exports.
*/
import type { CompanyPortabilityManifest } from "@paperclipai/shared";
const ROLE_LABELS: Record<string, string> = {
ceo: "CEO",
cto: "CTO",
cmo: "CMO",
cfo: "CFO",
coo: "COO",
vp: "VP",
manager: "Manager",
engineer: "Engineer",
agent: "Agent",
};
/**
* Generate a Mermaid flowchart (TD = top-down) representing the org chart.
* Returns null if there are no agents.
*/
export function generateOrgChartMermaid(agents: CompanyPortabilityManifest["agents"]): string | null {
if (agents.length === 0) return null;
const lines: string[] = [];
lines.push("```mermaid");
lines.push("graph TD");
// Node definitions with role labels
for (const agent of agents) {
const roleLabel = ROLE_LABELS[agent.role] ?? agent.role;
const id = mermaidId(agent.slug);
lines.push(` ${id}["${mermaidEscape(agent.name)}<br/><small>${mermaidEscape(roleLabel)}</small>"]`);
}
// Edges from parent to child
const slugSet = new Set(agents.map((a) => a.slug));
for (const agent of agents) {
if (agent.reportsToSlug && slugSet.has(agent.reportsToSlug)) {
lines.push(` ${mermaidId(agent.reportsToSlug)} --> ${mermaidId(agent.slug)}`);
}
}
lines.push("```");
return lines.join("\n");
}
/** Sanitize slug for use as a Mermaid node ID (alphanumeric + underscore). */
function mermaidId(slug: string): string {
return slug.replace(/[^a-zA-Z0-9_]/g, "_");
}
/** Escape text for Mermaid node labels. */
function mermaidEscape(s: string): string {
return s.replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
}
/**
* Generate the README.md content for a company export.
*/
export function generateReadme(
manifest: CompanyPortabilityManifest,
options: {
companyName: string;
companyDescription: string | null;
},
): string {
const lines: string[] = [];
lines.push(`# ${options.companyName}`);
lines.push("");
if (options.companyDescription) {
lines.push(`> ${options.companyDescription}`);
lines.push("");
}
// Org chart as Mermaid diagram
const mermaid = generateOrgChartMermaid(manifest.agents);
if (mermaid) {
lines.push(mermaid);
lines.push("");
}
// What's Inside table
lines.push("## What's Inside");
lines.push("");
lines.push("This is an [Agent Company](https://paperclip.ing) package.");
lines.push("");
const counts: Array<[string, number]> = [];
if (manifest.agents.length > 0) counts.push(["Agents", manifest.agents.length]);
if (manifest.projects.length > 0) counts.push(["Projects", manifest.projects.length]);
if (manifest.skills.length > 0) counts.push(["Skills", manifest.skills.length]);
if (manifest.issues.length > 0) counts.push(["Tasks", manifest.issues.length]);
if (counts.length > 0) {
lines.push("| Content | Count |");
lines.push("|---------|-------|");
for (const [label, count] of counts) {
lines.push(`| ${label} | ${count} |`);
}
lines.push("");
}
// Agents table
if (manifest.agents.length > 0) {
lines.push("### Agents");
lines.push("");
lines.push("| Agent | Role | Reports To |");
lines.push("|-------|------|------------|");
for (const agent of manifest.agents) {
const roleLabel = ROLE_LABELS[agent.role] ?? agent.role;
const reportsTo = agent.reportsToSlug ?? "\u2014";
lines.push(`| ${agent.name} | ${roleLabel} | ${reportsTo} |`);
}
lines.push("");
}
// Projects list
if (manifest.projects.length > 0) {
lines.push("### Projects");
lines.push("");
for (const project of manifest.projects) {
const desc = project.description ? ` \u2014 ${project.description}` : "";
lines.push(`- **${project.name}**${desc}`);
}
lines.push("");
}
// Getting Started
lines.push("## Getting Started");
lines.push("");
lines.push("```bash");
lines.push("pnpm paperclipai company import this-github-url-or-folder");
lines.push("```");
lines.push("");
lines.push("See [Paperclip](https://paperclip.ing) for more information.");
lines.push("");
// Footer
lines.push("---");
lines.push(`Exported from [Paperclip](https://paperclip.ing) on ${new Date().toISOString().split("T")[0]}`);
lines.push("");
return lines.join("\n");
}