Add company logo portability support

This commit is contained in:
dotta
2026-03-19 07:24:04 -05:00
parent 6d564e0539
commit 7a652b8998
6 changed files with 315 additions and 13 deletions

View File

@@ -628,6 +628,16 @@ function normalizeFileMap(
return out;
}
function pickTextFiles(files: Record<string, CompanyPortabilityFileEntry>) {
const out: Record<string, string> = {};
for (const [filePath, content] of Object.entries(files)) {
if (typeof content === "string") {
out[filePath] = content;
}
}
return out;
}
function collectSelectedExportSlugs(selectedFiles: Set<string>) {
const agents = new Set<string>();
const projects = new Set<string>();
@@ -707,7 +717,12 @@ function filterPortableExtensionYaml(yaml: string, selectedFiles: Set<string>) {
}
flushSection();
return out.join("\n");
let filtered = out.join("\n");
const logoPathMatch = filtered.match(/^\s{2}logoPath:\s*["']?([^"'\n]+)["']?\s*$/m);
if (logoPathMatch && !selectedFiles.has(logoPathMatch[1]!)) {
filtered = filtered.replace(/^\s{2}logoPath:\s*["']?([^"'\n]+)["']?\s*\n?/m, "");
}
return filtered;
}
function filterExportFiles(
@@ -956,6 +971,7 @@ const YAML_KEY_PRIORITY = [
"icon",
"capabilities",
"brandColor",
"logoPath",
"adapter",
"runtime",
"permissions",
@@ -1105,7 +1121,7 @@ function applySelectedFilesToSource(source: ResolvedSource, selectedFiles?: stri
throw unprocessable("Company package is missing COMPANY.md");
}
const effectiveFiles: Record<string, string> = {};
const effectiveFiles: Record<string, CompanyPortabilityFileEntry> = {};
for (const [filePath, content] of Object.entries(source.files)) {
const normalizedPath = normalizePortablePath(filePath);
if (!normalizedSelection.has(normalizedPath)) continue;
@@ -1993,10 +2009,11 @@ export function companyPortabilityService(db: Db, storage?: StorageService) {
const company = await companies.getById(companyId);
if (!company) throw notFound("Company not found");
const files: Record<string, string> = {};
const files: Record<string, CompanyPortabilityFileEntry> = {};
const warnings: string[] = [];
const envInputs: CompanyPortabilityManifest["envInputs"] = [];
const rootPath = normalizeAgentUrlKey(company.name) ?? "company-package";
let companyLogoPath: string | null = null;
const allAgentRows = include.agents ? await agents.list(companyId, { includeTerminated: true }) : [];
const liveAgentRows = allAgentRows.filter((agent) => agent.status !== "terminated");
@@ -2165,6 +2182,26 @@ export function companyPortabilityService(db: Db, storage?: StorageService) {
companyBodySections.join("\n\n").trim(),
);
if (include.company && company.logoAssetId) {
if (!storage) {
warnings.push("Skipped company logo from export because storage is unavailable.");
} else {
const logoAsset = await assetRecords.getById(company.logoAssetId);
if (!logoAsset) {
warnings.push(`Skipped company logo ${company.logoAssetId} because the asset record was not found.`);
} else {
try {
const object = await storage.getObject(company.id, logoAsset.objectKey);
const body = await streamToBuffer(object.stream);
companyLogoPath = `images/${COMPANY_LOGO_FILE_NAME}${resolveCompanyLogoExtension(logoAsset.contentType, logoAsset.originalFilename)}`;
files[companyLogoPath] = bufferToPortableBinaryFile(body, logoAsset.contentType);
} catch (err) {
warnings.push(`Failed to export company logo ${company.logoAssetId}: ${err instanceof Error ? err.message : String(err)}`);
}
}
}
}
const paperclipAgentsOut: Record<string, Record<string, unknown>> = {};
const paperclipProjectsOut: Record<string, Record<string, unknown>> = {};
const paperclipTasksOut: Record<string, Record<string, unknown>> = {};
@@ -2359,6 +2396,7 @@ export function companyPortabilityService(db: Db, storage?: StorageService) {
schema: "paperclip/v1",
company: stripEmptyValues({
brandColor: company.brandColor ?? null,
logoPath: companyLogoPath,
requireBoardApprovalForNewAgents: company.requireBoardApprovalForNewAgents ? undefined : false,
}),
agents: Object.keys(paperclipAgents).length > 0 ? paperclipAgents : undefined,
@@ -2506,7 +2544,7 @@ export function companyPortabilityService(db: Db, storage?: StorageService) {
for (const agent of selectedAgents) {
const filePath = ensureMarkdownPath(agent.path);
const markdown = source.files[filePath];
const markdown = readPortableTextFile(source.files, filePath);
if (typeof markdown !== "string") {
errors.push(`Missing markdown file for agent ${agent.slug}: ${filePath}`);
continue;
@@ -2525,7 +2563,7 @@ export function companyPortabilityService(db: Db, storage?: StorageService) {
if (include.projects) {
for (const project of manifest.projects) {
const markdown = source.files[ensureMarkdownPath(project.path)];
const markdown = readPortableTextFile(source.files, ensureMarkdownPath(project.path));
if (typeof markdown !== "string") {
errors.push(`Missing markdown file for project ${project.slug}: ${project.path}`);
continue;
@@ -2539,7 +2577,7 @@ export function companyPortabilityService(db: Db, storage?: StorageService) {
if (include.issues) {
for (const issue of manifest.issues) {
const markdown = source.files[ensureMarkdownPath(issue.path)];
const markdown = readPortableTextFile(source.files, ensureMarkdownPath(issue.path));
if (typeof markdown !== "string") {
errors.push(`Missing markdown file for task ${issue.slug}: ${issue.path}`);
continue;
@@ -2861,6 +2899,55 @@ export function companyPortabilityService(db: Db, storage?: StorageService) {
if (!targetCompany) throw notFound("Target company not found");
if (include.company) {
const logoPath = sourceManifest.company?.logoPath ?? null;
if (!logoPath) {
const cleared = await companies.update(targetCompany.id, { logoAssetId: null });
targetCompany = cleared ?? targetCompany;
} else {
const logoFile = plan.source.files[logoPath];
if (!logoFile) {
warnings.push(`Skipped company logo import because ${logoPath} is missing from the package.`);
} else if (!storage) {
warnings.push("Skipped company logo import because storage is unavailable.");
} else {
const contentType = isPortableBinaryFile(logoFile)
? (logoFile.contentType ?? inferContentTypeFromPath(logoPath))
: inferContentTypeFromPath(logoPath);
if (!contentType || !COMPANY_LOGO_CONTENT_TYPE_EXTENSIONS[contentType]) {
warnings.push(`Skipped company logo import for ${logoPath} because the file type is unsupported.`);
} else {
try {
const body = portableFileToBuffer(logoFile, logoPath);
const stored = await storage.putFile({
companyId: targetCompany.id,
namespace: "assets/companies",
originalFilename: path.posix.basename(logoPath),
contentType,
body,
});
const createdAsset = await assetRecords.create(targetCompany.id, {
provider: stored.provider,
objectKey: stored.objectKey,
contentType: stored.contentType,
byteSize: stored.byteSize,
sha256: stored.sha256,
originalFilename: stored.originalFilename,
createdByAgentId: null,
createdByUserId: actorUserId ?? null,
});
const updated = await companies.update(targetCompany.id, {
logoAssetId: createdAsset.id,
});
targetCompany = updated ?? targetCompany;
} catch (err) {
warnings.push(`Failed to import company logo ${logoPath}: ${err instanceof Error ? err.message : String(err)}`);
}
}
}
}
}
const resultAgents: CompanyPortabilityImportResult["agents"] = [];
const importedSlugToAgentId = new Map<string, string>();
const existingSlugToAgentId = new Map<string, string>();
@@ -2875,7 +2962,7 @@ export function companyPortabilityService(db: Db, storage?: StorageService) {
existingProjectSlugToId.set(existing.urlKey, existing.id);
}
const importedSkills = await companySkills.importPackageFiles(targetCompany.id, plan.source.files, {
const importedSkills = await companySkills.importPackageFiles(targetCompany.id, pickTextFiles(plan.source.files), {
onConflict: resolveSkillConflictStrategy(mode, plan.collisionStrategy),
});
const desiredSkillRefMap = new Map<string, string>();
@@ -2908,9 +2995,11 @@ export function companyPortabilityService(db: Db, storage?: StorageService) {
const bundleFiles = Object.fromEntries(
Object.entries(plan.source.files)
.filter(([filePath]) => filePath.startsWith(bundlePrefix))
.map(([filePath, content]) => [normalizePortablePath(filePath.slice(bundlePrefix.length)), content]),
.flatMap(([filePath, content]) => typeof content === "string"
? [[normalizePortablePath(filePath.slice(bundlePrefix.length)), content] as const]
: []),
);
const markdownRaw = bundleFiles["AGENTS.md"] ?? plan.source.files[manifestAgent.path];
const markdownRaw = bundleFiles["AGENTS.md"] ?? readPortableTextFile(plan.source.files, manifestAgent.path);
const fallbackPromptTemplate = asString((manifestAgent.adapterConfig as Record<string, unknown>).promptTemplate) || "";
if (!markdownRaw && fallbackPromptTemplate) {
bundleFiles["AGENTS.md"] = fallbackPromptTemplate;
@@ -3065,7 +3154,7 @@ export function companyPortabilityService(db: Db, storage?: StorageService) {
if (include.issues) {
for (const manifestIssue of sourceManifest.issues) {
const markdownRaw = plan.source.files[manifestIssue.path];
const markdownRaw = readPortableTextFile(plan.source.files, manifestIssue.path);
const parsed = markdownRaw ? parseFrontmatterMarkdown(markdownRaw) : null;
const description = parsed?.body || manifestIssue.description || null;
const assigneeAgentId = manifestIssue.assigneeAgentSlug