added a script to generate company assets
This commit is contained in:
parent
d8b408625e
commit
8fa4b6a5fb
1 changed files with 355 additions and 0 deletions
355
scripts/generate-company-assets.ts
Normal file
355
scripts/generate-company-assets.ts
Normal file
|
|
@ -0,0 +1,355 @@
|
|||
#!/usr/bin/env npx tsx
|
||||
/**
|
||||
* Generate org chart images and READMEs for agent company packages.
|
||||
*
|
||||
* Reads company packages from a directory, builds manifest-like data,
|
||||
* then uses the existing server-side SVG renderer (sharp, no browser)
|
||||
* and README generator.
|
||||
*
|
||||
* Usage:
|
||||
* npx tsx scripts/generate-company-assets.ts /path/to/companies-repo
|
||||
*
|
||||
* Processes each subdirectory that contains a COMPANY.md file.
|
||||
*/
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
import { renderOrgChartPng, type OrgNode } from "../server/src/routes/org-chart-svg.js";
|
||||
import { generateReadme } from "../server/src/services/company-export-readme.js";
|
||||
import type { CompanyPortabilityManifest } from "@paperclipai/shared";
|
||||
|
||||
// ── YAML frontmatter parser (minimal, no deps) ──────────────────
|
||||
|
||||
function parseFrontmatter(content: string): { data: Record<string, unknown>; body: string } {
|
||||
const match = content.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
|
||||
if (!match) return { data: {}, body: content };
|
||||
const yamlStr = match[1];
|
||||
const body = match[2];
|
||||
const data: Record<string, unknown> = {};
|
||||
|
||||
let currentKey: string | null = null;
|
||||
let currentValue: string | string[] | null = null;
|
||||
let inList = false;
|
||||
|
||||
for (const line of yamlStr.split("\n")) {
|
||||
// List item
|
||||
if (inList && /^\s+-\s+/.test(line)) {
|
||||
const val = line.replace(/^\s+-\s+/, "").trim();
|
||||
(currentValue as string[]).push(val);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Save previous key
|
||||
if (currentKey !== null && currentValue !== null) {
|
||||
data[currentKey] = currentValue;
|
||||
}
|
||||
inList = false;
|
||||
|
||||
// Key: value line
|
||||
const kvMatch = line.match(/^(\w[\w-]*)\s*:\s*(.*)$/);
|
||||
if (kvMatch) {
|
||||
currentKey = kvMatch[1];
|
||||
let val = kvMatch[2].trim();
|
||||
|
||||
if (val === "" || val === ">") {
|
||||
// Could be a multi-line value or list — peek ahead handled by next iterations
|
||||
currentValue = "";
|
||||
continue;
|
||||
}
|
||||
|
||||
if (val === "null" || val === "~") {
|
||||
currentValue = null;
|
||||
data[currentKey] = null;
|
||||
currentKey = null;
|
||||
currentValue = null;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Remove surrounding quotes
|
||||
if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) {
|
||||
val = val.slice(1, -1);
|
||||
}
|
||||
|
||||
currentValue = val;
|
||||
} else if (currentKey !== null && line.match(/^\s+-\s+/)) {
|
||||
// Start of list
|
||||
inList = true;
|
||||
currentValue = [];
|
||||
const val = line.replace(/^\s+-\s+/, "").trim();
|
||||
(currentValue as string[]).push(val);
|
||||
} else if (currentKey !== null && line.match(/^\s+\S/)) {
|
||||
// Continuation of multi-line scalar
|
||||
const trimmed = line.trim();
|
||||
if (typeof currentValue === "string") {
|
||||
currentValue = currentValue ? `${currentValue} ${trimmed}` : trimmed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Save last key
|
||||
if (currentKey !== null && currentValue !== null) {
|
||||
data[currentKey] = currentValue;
|
||||
}
|
||||
|
||||
return { data, body };
|
||||
}
|
||||
|
||||
// ── Slug to role mapping ─────────────────────────────────────────
|
||||
|
||||
const SLUG_TO_ROLE: Record<string, string> = {
|
||||
ceo: "ceo",
|
||||
cto: "cto",
|
||||
cmo: "cmo",
|
||||
cfo: "cfo",
|
||||
coo: "coo",
|
||||
};
|
||||
|
||||
function inferRole(slug: string, title: string | null): string {
|
||||
// Check direct slug match first
|
||||
if (SLUG_TO_ROLE[slug]) return SLUG_TO_ROLE[slug];
|
||||
|
||||
// Check title for C-suite
|
||||
const t = (title || "").toLowerCase();
|
||||
if (t.includes("chief executive")) return "ceo";
|
||||
if (t.includes("chief technology")) return "cto";
|
||||
if (t.includes("chief marketing")) return "cmo";
|
||||
if (t.includes("chief financial")) return "cfo";
|
||||
if (t.includes("chief operating")) return "coo";
|
||||
if (t.includes("vp") || t.includes("vice president")) return "vp";
|
||||
if (t.includes("manager")) return "manager";
|
||||
if (t.includes("qa") || t.includes("quality")) return "engineer";
|
||||
|
||||
// Default to engineer
|
||||
return "engineer";
|
||||
}
|
||||
|
||||
// ── Parse a company package directory ────────────────────────────
|
||||
|
||||
interface CompanyPackage {
|
||||
dir: string;
|
||||
name: string;
|
||||
description: string | null;
|
||||
slug: string;
|
||||
agents: CompanyPortabilityManifest["agents"];
|
||||
skills: CompanyPortabilityManifest["skills"];
|
||||
}
|
||||
|
||||
function parseCompanyPackage(companyDir: string): CompanyPackage | null {
|
||||
const companyMdPath = path.join(companyDir, "COMPANY.md");
|
||||
if (!fs.existsSync(companyMdPath)) return null;
|
||||
|
||||
const companyMd = fs.readFileSync(companyMdPath, "utf-8");
|
||||
const { data: companyData } = parseFrontmatter(companyMd);
|
||||
|
||||
const name = (companyData.name as string) || path.basename(companyDir);
|
||||
const description = (companyData.description as string) || null;
|
||||
const slug = (companyData.slug as string) || path.basename(companyDir);
|
||||
|
||||
// Parse agents
|
||||
const agentsDir = path.join(companyDir, "agents");
|
||||
const agents: CompanyPortabilityManifest["agents"] = [];
|
||||
if (fs.existsSync(agentsDir)) {
|
||||
for (const agentSlug of fs.readdirSync(agentsDir)) {
|
||||
const agentMdPath = path.join(agentsDir, agentSlug, "AGENTS.md");
|
||||
if (!fs.existsSync(agentMdPath)) continue;
|
||||
|
||||
const agentMd = fs.readFileSync(agentMdPath, "utf-8");
|
||||
const { data: agentData } = parseFrontmatter(agentMd);
|
||||
|
||||
const agentName = (agentData.name as string) || agentSlug;
|
||||
const title = (agentData.title as string) || null;
|
||||
const reportsTo = agentData.reportsTo as string | null;
|
||||
const skills = (agentData.skills as string[]) || [];
|
||||
const role = inferRole(agentSlug, title);
|
||||
|
||||
agents.push({
|
||||
slug: agentSlug,
|
||||
name: agentName,
|
||||
path: `agents/${agentSlug}/AGENTS.md`,
|
||||
skills,
|
||||
role,
|
||||
title,
|
||||
icon: null,
|
||||
capabilities: null,
|
||||
reportsToSlug: reportsTo || null,
|
||||
adapterType: "claude_local",
|
||||
adapterConfig: {},
|
||||
runtimeConfig: {},
|
||||
permissions: {},
|
||||
budgetMonthlyCents: 0,
|
||||
metadata: null,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Parse skills
|
||||
const skillsDir = path.join(companyDir, "skills");
|
||||
const skills: CompanyPortabilityManifest["skills"] = [];
|
||||
if (fs.existsSync(skillsDir)) {
|
||||
for (const skillSlug of fs.readdirSync(skillsDir)) {
|
||||
const skillMdPath = path.join(skillsDir, skillSlug, "SKILL.md");
|
||||
if (!fs.existsSync(skillMdPath)) continue;
|
||||
|
||||
const skillMd = fs.readFileSync(skillMdPath, "utf-8");
|
||||
const { data: skillData } = parseFrontmatter(skillMd);
|
||||
|
||||
const skillName = (skillData.name as string) || skillSlug;
|
||||
const skillDesc = (skillData.description as string) || null;
|
||||
|
||||
// Extract source info from metadata
|
||||
let sourceType = "local";
|
||||
let sourceLocator: string | null = null;
|
||||
const metadata = skillData.metadata as Record<string, unknown> | undefined;
|
||||
if (metadata) {
|
||||
// metadata.sources is parsed as a nested structure, but our simple parser
|
||||
// doesn't handle it well. Check for github repo in the raw SKILL.md instead.
|
||||
const repoMatch = skillMd.match(/repo:\s*(.+)/);
|
||||
const pathMatch = skillMd.match(/path:\s*(.+)/);
|
||||
if (repoMatch) {
|
||||
sourceType = "github";
|
||||
const repo = repoMatch[1].trim();
|
||||
const filePath = pathMatch ? pathMatch[1].trim() : "";
|
||||
sourceLocator = `https://github.com/${repo}/blob/main/${filePath}`;
|
||||
}
|
||||
}
|
||||
|
||||
skills.push({
|
||||
key: skillSlug,
|
||||
slug: skillSlug,
|
||||
name: skillName,
|
||||
path: `skills/${skillSlug}/SKILL.md`,
|
||||
description: skillDesc,
|
||||
sourceType,
|
||||
sourceLocator,
|
||||
sourceRef: null,
|
||||
trustLevel: null,
|
||||
compatibility: null,
|
||||
metadata: null,
|
||||
fileInventory: [{ path: `skills/${skillSlug}/SKILL.md`, kind: "skill" }],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return { dir: companyDir, name, description, slug, agents, skills };
|
||||
}
|
||||
|
||||
// ── Build OrgNode tree from agents ───────────────────────────────
|
||||
|
||||
const ROLE_LABELS: Record<string, string> = {
|
||||
ceo: "Chief Executive",
|
||||
cto: "Technology",
|
||||
cmo: "Marketing",
|
||||
cfo: "Finance",
|
||||
coo: "Operations",
|
||||
vp: "VP",
|
||||
manager: "Manager",
|
||||
engineer: "Engineer",
|
||||
agent: "Agent",
|
||||
};
|
||||
|
||||
function buildOrgTree(agents: CompanyPortabilityManifest["agents"]): OrgNode[] {
|
||||
const bySlug = new Map(agents.map((a) => [a.slug, a]));
|
||||
const childrenOf = new Map<string | null, typeof agents>();
|
||||
for (const a of agents) {
|
||||
const parent = a.reportsToSlug ?? null;
|
||||
const list = childrenOf.get(parent) ?? [];
|
||||
list.push(a);
|
||||
childrenOf.set(parent, list);
|
||||
}
|
||||
const build = (parentSlug: string | null): OrgNode[] => {
|
||||
const members = childrenOf.get(parentSlug) ?? [];
|
||||
return members.map((m) => ({
|
||||
id: m.slug,
|
||||
name: m.name,
|
||||
role: ROLE_LABELS[m.role] ?? m.role,
|
||||
status: "active",
|
||||
reports: build(m.slug),
|
||||
}));
|
||||
};
|
||||
const roots = agents.filter((a) => !a.reportsToSlug || !bySlug.has(a.reportsToSlug));
|
||||
const tree = build(null);
|
||||
for (const root of roots) {
|
||||
if (root.reportsToSlug && !bySlug.has(root.reportsToSlug)) {
|
||||
tree.push({
|
||||
id: root.slug,
|
||||
name: root.name,
|
||||
role: ROLE_LABELS[root.role] ?? root.role,
|
||||
status: "active",
|
||||
reports: build(root.slug),
|
||||
});
|
||||
}
|
||||
}
|
||||
return tree;
|
||||
}
|
||||
|
||||
// ── Main ─────────────────────────────────────────────────────────
|
||||
|
||||
async function main() {
|
||||
const companiesDir = process.argv[2];
|
||||
if (!companiesDir) {
|
||||
console.error("Usage: npx tsx scripts/generate-company-assets.ts <companies-dir>");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const resolvedDir = path.resolve(companiesDir);
|
||||
if (!fs.existsSync(resolvedDir)) {
|
||||
console.error(`Directory not found: ${resolvedDir}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const entries = fs.readdirSync(resolvedDir, { withFileTypes: true });
|
||||
let processed = 0;
|
||||
|
||||
for (const entry of entries) {
|
||||
if (!entry.isDirectory()) continue;
|
||||
const companyDir = path.join(resolvedDir, entry.name);
|
||||
const pkg = parseCompanyPackage(companyDir);
|
||||
if (!pkg) continue;
|
||||
|
||||
console.log(`\n── ${pkg.name} (${pkg.slug}) ──`);
|
||||
console.log(` ${pkg.agents.length} agents, ${pkg.skills.length} skills`);
|
||||
|
||||
// Generate org chart PNG
|
||||
if (pkg.agents.length > 0) {
|
||||
const orgTree = buildOrgTree(pkg.agents);
|
||||
console.log(` Org tree roots: ${orgTree.map((n) => n.name).join(", ")}`);
|
||||
|
||||
const pngBuffer = await renderOrgChartPng(orgTree, "warmth");
|
||||
const imagesDir = path.join(companyDir, "images");
|
||||
fs.mkdirSync(imagesDir, { recursive: true });
|
||||
const pngPath = path.join(imagesDir, "org-chart.png");
|
||||
fs.writeFileSync(pngPath, pngBuffer);
|
||||
console.log(` ✓ ${path.relative(resolvedDir, pngPath)} (${(pngBuffer.length / 1024).toFixed(1)}kb)`);
|
||||
}
|
||||
|
||||
// Generate README
|
||||
const manifest: CompanyPortabilityManifest = {
|
||||
schemaVersion: 1,
|
||||
generatedAt: new Date().toISOString(),
|
||||
source: null,
|
||||
includes: { company: true, agents: true, projects: false, issues: false, skills: true },
|
||||
company: null,
|
||||
agents: pkg.agents,
|
||||
skills: pkg.skills,
|
||||
projects: [],
|
||||
issues: [],
|
||||
envInputs: [],
|
||||
};
|
||||
|
||||
const readme = generateReadme(manifest, {
|
||||
companyName: pkg.name,
|
||||
companyDescription: pkg.description,
|
||||
});
|
||||
const readmePath = path.join(companyDir, "README.md");
|
||||
fs.writeFileSync(readmePath, readme);
|
||||
console.log(` ✓ ${path.relative(resolvedDir, readmePath)}`);
|
||||
|
||||
processed++;
|
||||
}
|
||||
|
||||
console.log(`\n✓ Processed ${processed} companies.`);
|
||||
}
|
||||
|
||||
main().catch((e) => {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue