#!/usr/bin/env -S npx --yes tsx // ============================================================================= // render.ts — Render a template with vault variable substitution // // Part of: .opencode/skills/templating-vault/scripts/ // // Two modes: // 1. File mode (--output-file): Renders template, writes to file, returns path // 2. Prepare mode (--prepare --id): Renders template, stores in vault under // prepared., returns only the ID. For vault-aware prompt-by-reference. // // USAGE: // # File mode (vault-unaware agents) // render.ts --template-key --scope --output-file // // # Prepare mode (vault-aware agents) // render.ts --template-key --scope --prepare --id // // # Template from file instead of vault // render.ts --template-file --scope --output-file // // OUTPUT (stdout): JSON result // ============================================================================= import * as fs from 'node:fs'; import { fileURLToPath } from 'node:url'; import yaml from 'js-yaml'; import { parseCommonFlags, getFlag, hasFlag, output, log, loadConfig, readValue, getValue, writeValue, loadSchema, saveKeyMeta, listAllKeys, loadKeyMeta, readValue, evaluateTemplate, atomicWrite, type TemplateSchema, type KeyMeta, } from './api.ts'; async function main(): Promise { const { config: cfgPath, storeDirs, rest } = parseCommonFlags(process.argv); const config = loadConfig(cfgPath, storeDirs.length > 0 ? storeDirs : undefined); const templateKey = getFlag(rest, '--template-key'); const templateFile = getFlag(rest, '--template-file'); const outputFile = getFlag(rest, '--output-file'); const prepare = hasFlag(rest, '--prepare'); let id = getFlag(rest, '--id'); if (!templateKey && !templateFile) { output({ rendered: false, error: '--template-key or --template-file is required' }); process.exit(1); } // Auto-generate ID if --prepare but no --id if (prepare && !id) { id = `prep-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`; } if (!prepare && !outputFile) { output({ rendered: false, error: '--output-file is required when not using --prepare' }); process.exit(1); } // Load template text let templateText: string; let schema: TemplateSchema | null = null; if (templateKey) { const raw = readValue(config, templateKey); if (raw === null) { output({ rendered: false, error: `Template key "${templateKey}" not found in vault` }); process.exit(1); } templateText = raw; schema = loadSchema(config, templateKey); } else { if (!fs.existsSync(templateFile!)) { output({ rendered: false, error: `Template file "${templateFile}" not found` }); process.exit(1); } templateText = fs.readFileSync(templateFile!, 'utf-8'); } // Collect variables for rendering const variables: Record = {}; const substituted: string[] = []; const missing: string[] = []; // Collect RAW values from all vault stores (skip templates to avoid recursion) const allKeys = listAllKeys(config, '', true); for (const k of allKeys) { const meta = loadKeyMeta(config, k); if (meta.template) continue; // Skip computed/template keys const rawVal = readValue(config, k); if (rawVal !== null) { const nunjucksKey = k.replace(/\./g, '_'); variables[nunjucksKey] = rawVal; const shortName = k.split('.').pop()!; if (!(shortName in variables)) { variables[shortName] = rawVal; } } } // Detect which placeholders are in the template const placeholderRegex = /\{\{\s*(\w+)(?:\s*\|[^}]*)?\s*\}\}/g; let match: RegExpExecArray | null; const foundPlaceholders = new Set(); while ((match = placeholderRegex.exec(templateText)) !== null) { foundPlaceholders.add(match[1]); } for (const ph of foundPlaceholders) { if (ph in variables) { substituted.push(ph); } else { missing.push(ph); } } // Render template let rendered: string; try { rendered = evaluateTemplate(templateText, variables); } catch (err) { output({ rendered: false, error: `Template rendering failed: ${err instanceof Error ? err.message : String(err)}`, }); process.exit(1); } if (prepare) { // Store in vault under prepared. const preparedKey = `prepared.${id}`; writeValue(config, preparedKey, rendered); // Store metadata const meta: KeyMeta = { template: false, owner: 'system', description: `Prepared prompt from template ${templateKey ?? templateFile}`, }; saveKeyMeta(config, preparedKey, meta); // Store schema if available if (schema) { const schemaKey = `prepared.${id}.schema`; writeValue(config, schemaKey, JSON.stringify(schema, null, 2)); } // Store template source reference const refKey = `prepared.${id}.template_ref`; writeValue(config, refKey, templateKey ?? templateFile ?? 'unknown'); output({ prepared: true, id, key: preparedKey, substituted, missing, }); } else { // Write to file const outDir = require('node:path').dirname(outputFile!); fs.mkdirSync(outDir, { recursive: true }); atomicWrite(outputFile!, rendered); output({ rendered: true, output_file: outputFile, substituted, missing, }); } } if (process.argv[1] === fileURLToPath(import.meta.url)) { main().catch((err: unknown) => { output({ rendered: false, error: String(err instanceof Error ? err.message : err) }); process.exit(1); }); }