1885990081
CI / build (push) Waiting to run
CI / docker (push) Blocked by required conditions
CI / helm (push) Waiting to run
CI / push-validation (push) Waiting to run
CI / status-check (push) Blocked by required conditions
CI / lint (push) Waiting to run
CI / typecheck (push) Waiting to run
CI / security (push) Waiting to run
CI / quality (push) Waiting to run
CI / unit_tests (push) Waiting to run
CI / integration_tests (push) Waiting to run
CI / e2e_tests (push) Waiting to run
CI / coverage (push) Blocked by required conditions
CI / benchmark-publish (push) Waiting to run
180 lines
5.5 KiB
TypeScript
180 lines
5.5 KiB
TypeScript
#!/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.<id>, returns only the ID. For vault-aware prompt-by-reference.
|
|
//
|
|
// USAGE:
|
|
// # File mode (vault-unaware agents)
|
|
// render.ts --template-key <key> --scope <tag> --output-file <path>
|
|
//
|
|
// # Prepare mode (vault-aware agents)
|
|
// render.ts --template-key <key> --scope <tag> --prepare --id <id>
|
|
//
|
|
// # Template from file instead of vault
|
|
// render.ts --template-file <path> --scope <tag> --output-file <path>
|
|
//
|
|
// 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<void> {
|
|
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<string, unknown> = {};
|
|
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<string>();
|
|
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.<id>
|
|
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);
|
|
});
|
|
}
|