- 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>
147 lines
4.2 KiB
TypeScript
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, """).replace(/</g, "<").replace(/>/g, ">");
|
|
}
|
|
|
|
/**
|
|
* 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");
|
|
}
|