/**
* Template generators for wpnav init
*
* Provides template rendering and file generation utilities
* for the init wizard (CLAUDE.md, .mcp.json, etc.)
*
* For compiled binaries (Bun compile), templates are loaded from
* embedded assets instead of filesystem.
*/
import * as fs from 'fs';
import * as path from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// =============================================================================
// Embedded Asset Support (for Bun-compiled binaries)
// =============================================================================
/**
* Cached embedded templates (loaded lazily on first access).
* null = not yet checked, undefined = checked but not available
*/
let embeddedTemplatesCache: Record<string, string> | null | undefined = null;
/**
* Get embedded templates if available.
* Uses lazy loading to avoid top-level await issues.
* Returns null if not in binary mode.
*/
function getEmbeddedTemplates(): Record<string, string> | null {
// Return cached result if already checked (undefined means checked but not found)
if (embeddedTemplatesCache !== null) {
return embeddedTemplatesCache === undefined ? null : embeddedTemplatesCache;
}
// Try to load embedded assets synchronously
// This works because embedded-assets.ts exports pure data (no async)
try {
// Use require for synchronous loading
// eslint-disable-next-line @typescript-eslint/no-require-imports
const embedded = require('../../embedded-assets.js');
if (embedded.IS_EMBEDDED && embedded.EMBEDDED_TEMPLATES) {
const templates: Record<string, string> = embedded.EMBEDDED_TEMPLATES;
embeddedTemplatesCache = templates;
return templates;
}
} catch {
// Not in binary mode - filesystem fallback will be used
}
embeddedTemplatesCache = undefined;
return null;
}
/**
* Context for CLAUDE.md template rendering
*/
export interface ClaudeMdContext {
site_name?: string;
site_url?: string;
environment: string;
generated_date: string;
mcp_version: string;
}
/**
* Options for .mcp.json generation
*/
export interface McpJsonOptions {
/** Path to wpnav config file (default: ./wpnav.config.json) */
configPath?: string;
/** Enable write operations (default: false) */
enableWrites?: boolean;
}
/**
* Simple template renderer using regex-based replacement
*
* Supports:
* - {{variable}} - Simple variable substitution
* - {{#if var}}...{{/if}} - Conditional blocks (renders content if var is truthy)
*
* @param template - Template string with placeholders
* @param context - Object with values to substitute
* @returns Rendered template string
*/
export function renderTemplate(template: string, context: Record<string, unknown>): string {
let result = template;
// Handle {{#if var}}...{{/if}} blocks
// Matches: {{#if varName}}content{{/if}}
result = result.replace(
/\{\{#if\s+(\w+)\}\}([\s\S]*?)\{\{\/if\}\}/g,
(_match, varName: string, content: string) => {
const value = context[varName];
// Render content if value is truthy (not undefined, null, false, '', 0)
return value ? content : '';
}
);
// Handle {{variable}} substitutions
result = result.replace(/\{\{(\w+)\}\}/g, (_match, varName: string) => {
const value = context[varName];
// Return empty string for undefined/null, otherwise convert to string
return value !== undefined && value !== null ? String(value) : '';
});
return result;
}
/**
* Get the path to template files directory.
* Handles both development (src/) and installed (dist/) contexts.
* Templates are stored in src/cli/templates/ and included in package.json files.
*/
function getTemplatesPath(): string {
// In dist: dist/cli/init/generators.js
// Templates are at: src/cli/templates/
return path.join(__dirname, '..', '..', '..', 'src', 'cli', 'templates');
}
/**
* Load a template file from the templates directory.
* Uses embedded assets if available (binary mode), otherwise filesystem.
*
* @param templateName - Name of the template file (e.g., 'CLAUDE.md.template')
* @returns Template content as string
* @throws Error if template file not found
*/
export function loadTemplate(templateName: string): string {
// Try embedded templates first (binary mode)
const embeddedTemplates = getEmbeddedTemplates();
if (embeddedTemplates && templateName in embeddedTemplates) {
return embeddedTemplates[templateName];
}
// Fall back to filesystem (normal npm mode)
const templatePath = path.join(getTemplatesPath(), templateName);
try {
return fs.readFileSync(templatePath, 'utf8');
} catch (err) {
throw new Error(`Template not found: ${templateName} (looked in ${templatePath})`);
}
}
/**
* Generate CLAUDE.md content from template
*
* @param context - Template context with site info
* @returns Rendered CLAUDE.md content
*/
export function generateClaudeMd(context: ClaudeMdContext): string {
const template = loadTemplate('CLAUDE.md.template');
return renderTemplate(template, context as unknown as Record<string, unknown>);
}
/**
* Generate AGENTS.md content from template (OpenAI Codex)
*
* @param context - Template context with site info
* @returns Rendered AGENTS.md content
*/
export function generateAgentsMd(context: ClaudeMdContext): string {
const template = loadTemplate('AGENTS.md.template');
return renderTemplate(template, context as unknown as Record<string, unknown>);
}
/**
* Generate GEMINI.md content from template (Google Gemini CLI)
*
* @param context - Template context with site info
* @returns Rendered GEMINI.md content
*/
export function generateGeminiMd(context: ClaudeMdContext): string {
const template = loadTemplate('GEMINI.md.template');
return renderTemplate(template, context as unknown as Record<string, unknown>);
}
/**
* Get default CLAUDE.md context with current date and package version
*
* @param overrides - Optional context values to override defaults
* @returns Complete context for CLAUDE.md generation
*/
export function getDefaultClaudeMdContext(overrides?: Partial<ClaudeMdContext>): ClaudeMdContext {
return {
environment: 'local',
generated_date: new Date().toISOString().split('T')[0],
mcp_version: getPackageVersion(),
...overrides,
};
}
/**
* Get package version from package.json
* Falls back to 'unknown' if not found
*/
function getPackageVersion(): string {
try {
// Navigate from dist/cli/init/ to project root
const packagePath = path.join(__dirname, '..', '..', '..', 'package.json');
const packageJson = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
return packageJson.version || 'unknown';
} catch {
return 'unknown';
}
}
/**
* Generate .mcp.json content for Claude Code project-level MCP configuration
*
* This creates the Claude Code project MCP config format, which is different
* from mcp-config.json (the general MCP setup guide).
*
* @param options - Generation options
* @returns JSON string for .mcp.json file
*/
export function generateMcpJson(options: McpJsonOptions = {}): string {
const { configPath = './wpnav.config.json', enableWrites = false } = options;
return JSON.stringify(
{
mcpServers: {
wpnav: {
command: 'npx',
args: ['-y', '@littlebearapps/wp-navigator-mcp', configPath],
env: {
WPNAV_ENABLE_WRITES: enableWrites ? '1' : '0',
},
},
},
},
null,
2
);
}