/** * Brand templating — single source of truth for the operator-visible brand name. * * Skills, agent templates, plugin manifests, and reference content reference * the brand only via the literal `{{productName}}` placeholder. This module * substitutes at read time from `brand.json`. Same shadow-source defect class * as Tasks 787/788: there is no fallback; missing or empty `productName` * hard-fails so the wrong brand can never reach the system prompt or the * operator UI silently. * * Used by both `platform/ui` (admin/public agent system-prompt assembly) and * `platform/plugins/admin/mcp` (the `plugin-read` MCP tool). Both call sites * pass the absolute file path as `sourcePath` so the diagnostic line names * exactly which file was substituted. */ import { join } from "node:path"; import { existsSync, readFileSync } from "node:fs"; const PLACEHOLDER = "{{productName}}"; let cachedProductName: string | null = null; function brandJsonPath(): string { const platformRoot = process.env.MAXY_PLATFORM_ROOT; if (!platformRoot) { throw new Error( "[skill-loader] MAXY_PLATFORM_ROOT not set — cannot resolve brand.json", ); } return join(platformRoot, "config", "brand.json"); } export function getBrandProductName(): string { if (cachedProductName !== null) return cachedProductName; const path = brandJsonPath(); if (!existsSync(path)) { throw new Error(`[skill-loader] brand.json missing at ${path}`); } let parsed: { productName?: unknown }; try { parsed = JSON.parse(readFileSync(path, "utf-8")); } catch (err) { throw new Error( `[skill-loader] brand.json unreadable at ${path}: ${err instanceof Error ? err.message : String(err)}`, ); } const value = parsed.productName; if (typeof value !== "string" || value.trim() === "") { throw new Error( `[skill-loader] brand.json at ${path} has missing or empty productName`, ); } cachedProductName = value; return value; } export function substituteBrandPlaceholders(content: string, sourcePath: string): string { if (!content.includes(PLACEHOLDER)) return content; let productName: string; try { productName = getBrandProductName(); } catch (err) { console.error( `[skill-loader] ERROR: brand.json missing — cannot resolve productName for skill ${sourcePath}`, ); throw err; } // split-length counts non-overlapping literal matches without regex-escape risk. const occurrences = content.split(PLACEHOLDER).length - 1; const substituted = content.split(PLACEHOLDER).join(productName); console.log( `[skill-loader] brand-substituted productName=${productName} skill=${sourcePath} occurrences=${occurrences}`, ); return substituted; }