import { bootstrap } from "./bootstrap.js";
import type { Hash, Store } from "./types.js";
// ── Text templates ──────────────────────────────────────────────────
const TEXT_TEMPLATES: ReadonlyArray<
readonly [alias: string, template: string]
> = [
["@ocas/output/put", "{{ payload }}"],
[
"@ocas/output/get",
"type: {{ payload.type }}\ntimestamp: {{ payload.timestamp }}",
],
["@ocas/output/has", "{{ payload }}"],
["@ocas/output/hash", "{{ payload }}"],
["@ocas/output/verify", "{{ payload }}"],
["@ocas/output/refs", "{% for ref in payload %}{{ ref }}\n{% endfor %}"],
["@ocas/output/walk", "{% for item in payload %}{{ item }}\n{% endfor %}"],
["@ocas/output/list", "{% for item in payload %}{{ item }}\n{% endfor %}"],
[
"@ocas/output/list-meta",
"{% for item in payload %}{{ item.hash }} {{ item.created }} {{ item.updated }}\n{% endfor %}",
],
[
"@ocas/output/list-schema",
"{% for item in payload %}{{ item.hash }} {{ item.created }} {{ item.updated }}\n{% endfor %}",
],
[
"@ocas/output/var-set",
"name: {{ payload.name }}\nschema: {{ payload.schema }}\nvalue: {{ payload.value }}",
],
[
"@ocas/output/var-get",
"name: {{ payload.name }}\nschema: {{ payload.schema }}\nvalue: {{ payload.value }}",
],
[
"@ocas/output/var-delete",
"name: {{ payload.name }}\nschema: {{ payload.schema }}\nvalue: {{ payload.value }}",
],
[
"@ocas/output/var-list",
"{% for v in payload %}name: {{ v.name }}\nschema: {{ v.schema }}\nvalue: {{ v.value }}\n{% endfor %}",
],
[
"@ocas/output/tag",
"{% for t in payload %}{{ t.key }}{% if t.value %}:{{ t.value }}{% endif %}\n{% endfor %}",
],
[
"@ocas/output/untag",
"{% for t in payload %}{{ t.key }}{% if t.value %}:{{ t.value }}{% endif %}\n{% endfor %}",
],
[
"@ocas/output/var-history",
"name: {{ payload.name }}\nschema: {{ payload.schema }}\n{% for v in payload.values %}{{ forloop.index0 }}: {{ v }}\n{% endfor %}",
],
[
"@ocas/output/template-set",
"schemaHash: {{ payload.schemaHash }}\ncontentHash: {{ payload.contentHash }}",
],
["@ocas/output/template-get", "{{ payload }}"],
[
"@ocas/output/template-list",
"{% for t in payload %}schemaHash: {{ t.schemaHash }}\ncontentHash: {{ t.contentHash }}\n{% endfor %}",
],
["@ocas/output/template-delete", "deleted: {{ payload.deleted }}"],
[
"@ocas/output/gc",
"total: {{ payload.total }}\nreachable: {{ payload.reachable }}\ncollected: {{ payload.collected }}\nscanned: {{ payload.scanned }}",
],
[
"@ocas/output/export",
"nodes: {{ payload.nodes }}\nvars: {{ payload.vars }}\ntags: {{ payload.tags }}",
],
[
"@ocas/output/import",
"nodes.imported: {{ payload.nodes.imported }}\nnodes.skipped: {{ payload.nodes.skipped }}\nvars.created: {{ payload.vars.created }}\nvars.updated: {{ payload.vars.updated }}\ntags: {{ payload.tags }}",
],
];
// ── HTML templates ──────────────────────────────────────────────────
const HTML_TEMPLATES: ReadonlyArray<
readonly [alias: string, template: string]
> = [
// Simple value templates (card layout)
[
"@ocas/output/put",
'
' +
'' +
'
{{ payload }}
' +
"
",
],
[
"@ocas/output/has",
'' +
'' +
'
' +
'{% if payload %}✓ yes' +
'{% else %}✗ no{% endif %}' +
"
",
],
[
"@ocas/output/hash",
'' +
'' +
'
{{ payload }}
' +
"
",
],
[
"@ocas/output/verify",
'' +
'' +
'
' +
'{% if payload == "ok" %}✓ ok' +
'{% elsif payload == "corrupted" %}✗ corrupted' +
'{% else %}⚠ invalid{% endif %}' +
"
",
],
[
"@ocas/output/template-get",
'",
],
[
"@ocas/output/template-delete",
'' +
'' +
'
' +
'{% if payload.deleted %}✓ deleted' +
'{% else %}✗ not found{% endif %}' +
"
",
],
// Structured templates (card layout with key-value grid)
[
"@ocas/output/get",
'' +
'' +
'
' +
'- Type
{{ payload.type }} ' +
"- Timestamp
- {{ payload.timestamp }}
" +
"{% if payload.tags %}" +
"- Tags
- {% for t in payload.tags %}" +
'{{ t.key }}{% if t.value %}:{{ t.value }}{% endif %}' +
"{% endfor %}
" +
"{% endif %}" +
"
" +
"
",
],
[
"@ocas/output/var-set",
'' +
'' +
'
' +
"- Name
- {{ payload.name }}
" +
'- Schema
{{ payload.schema }} ' +
'- Value
{{ payload.value }} ' +
"
" +
"
",
],
[
"@ocas/output/var-get",
'' +
'' +
'
' +
"- Name
- {{ payload.name }}
" +
'- Schema
{{ payload.schema }} ' +
'- Value
{{ payload.value }} ' +
"{% if payload.valueTags %}" +
"- Tags
- {% for t in payload.valueTags %}" +
'{{ t.key }}{% if t.value %}:{{ t.value }}{% endif %}' +
"{% endfor %}
" +
"{% endif %}" +
"
" +
"
",
],
[
"@ocas/output/var-delete",
'' +
'' +
'
' +
"- Name
- {{ payload.name }}
" +
'- Schema
{{ payload.schema }} ' +
'- Value
{{ payload.value }} ' +
"
" +
"
",
],
[
"@ocas/output/template-set",
'' +
'' +
'
' +
'- Schema
{{ payload.schemaHash }} ' +
'- Content
{{ payload.contentHash }} ' +
"
" +
"
",
],
// List/Array templates
[
"@ocas/output/refs",
'' +
'' +
'
' +
'{% for ref in payload %}{{ ref }} {% endfor %}' +
"
",
],
[
"@ocas/output/walk",
'' +
'' +
'
' +
'{% for item in payload %}{{ item }} {% endfor %}' +
"
",
],
[
"@ocas/output/list",
'' +
'' +
'
| HASH | CREATED | UPDATED |
' +
"{% for item in payload %}" +
'{{ item.hash }} | ' +
'{{ item.created }} | ' +
'{{ item.updated }} | ' +
"
{% endfor %}
",
],
[
"@ocas/output/list-meta",
'' +
'' +
'
| HASH | CREATED | UPDATED |
' +
"{% for item in payload %}" +
'{{ item.hash }} | ' +
'{{ item.created }} | ' +
'{{ item.updated }} | ' +
"
{% endfor %}
",
],
[
"@ocas/output/list-schema",
'' +
'' +
'
| HASH | CREATED | UPDATED |
' +
"{% for item in payload %}" +
'{{ item.hash }} | ' +
'{{ item.created }} | ' +
'{{ item.updated }} | ' +
"
{% endfor %}
",
],
[
"@ocas/output/var-list",
'' +
'' +
'
| NAME | SCHEMA | VALUE |
' +
"{% for v in payload %}" +
'| {{ v.name }} | ' +
'{{ v.schema }} | ' +
'{{ v.value }} | ' +
"
{% endfor %}
",
],
[
"@ocas/output/var-history",
'' +
'' +
'
' +
"- Name
- {{ payload.name }}
" +
'- Schema
{{ payload.schema }} ' +
"
" +
'
' +
"{% for v in payload.values %}- " +
'
{{ v }}' +
'{% if forloop.first %} ← current{% endif %}' +
" {% endfor %}
" +
"
",
],
[
"@ocas/output/template-list",
'' +
'' +
'
| SCHEMA | CONTENT |
' +
"{% for t in payload %}" +
'{{ t.schemaHash }} | ' +
'{{ t.contentHash }} | ' +
"
{% endfor %}
",
],
[
"@ocas/output/tag",
'' +
'' +
'
| KEY | VALUE | TARGET |
' +
"{% for t in payload %}" +
"| {{ t.key }} | " +
'{% if t.value %}{{ t.value }}{% else %}—{% endif %} | ' +
'{{ t.target }} | ' +
"
{% endfor %}
",
],
[
"@ocas/output/untag",
'' +
'' +
'
| KEY | VALUE | TARGET |
' +
"{% for t in payload %}" +
"| {{ t.key }} | " +
'{% if t.value %}{{ t.value }}{% else %}—{% endif %} | ' +
'{{ t.target }} | ' +
"
{% endfor %}
",
],
// Statistics templates (card + stats-grid)
[
"@ocas/output/gc",
'' +
'' +
'
' +
'
{{ payload.total }}' +
'total nodes
' +
'
{{ payload.reachable }}' +
'reachable
' +
'
' +
'{% if payload.collected == 0 %}{{ payload.collected }}' +
'{% else %}{{ payload.collected }}{% endif %}' +
'collected
' +
'
{{ payload.scanned }}' +
'scanned
' +
"
",
],
[
"@ocas/output/export",
'' +
'' +
'
' +
'
{{ payload.nodes }}' +
'nodes
' +
'
{{ payload.vars }}' +
'variables
' +
'
{{ payload.tags }}' +
'tags
' +
"
",
],
[
"@ocas/output/import",
'' +
'' +
'
' +
'
{{ payload.nodes.imported }}' +
'nodes imported
' +
'
{{ payload.nodes.skipped }}' +
'nodes skipped
' +
'
{{ payload.vars.created }}' +
'variables created
' +
'
{{ payload.vars.updated }}' +
'variables updated
' +
'
{{ payload.tags }}' +
'tags
' +
"
",
],
];
// ── Shared CSS ──────────────────────────────────────────────────────
/**
* Shared CSS for output HTML templates.
* Uses `.ocas-` scoped class names and CSS custom properties per the design guide.
*/
const OUTPUT_CSS = [
// Design tokens
":root {" +
" --ocas-font: system-ui, -apple-system, 'Segoe UI', sans-serif;" +
" --ocas-mono: ui-monospace, 'SF Mono', 'Cascadia Code', monospace;" +
" --ocas-bg: #fafafa;" +
" --ocas-card-bg: #fff;" +
" --ocas-card-border: #e5e7eb;" +
" --ocas-card-shadow: 0 1px 3px rgba(0,0,0,0.06), 0 1px 2px rgba(0,0,0,0.04);" +
" --ocas-card-radius: 8px;" +
" --ocas-text: #1f2937;" +
" --ocas-text-muted: #6b7280;" +
" --ocas-green: #16a34a;" +
" --ocas-red: #dc2626;" +
" --ocas-yellow: #d97706;" +
" --ocas-hash-bg: #f3f4f6;" +
" --ocas-hash-text: #374151;" +
" --ocas-metric-size: 1.75rem;" +
" }",
// Card container
".ocas-card { background: var(--ocas-card-bg); border: 1px solid var(--ocas-card-border);" +
" box-shadow: var(--ocas-card-shadow); border-radius: var(--ocas-card-radius);" +
" font-family: var(--ocas-font); color: var(--ocas-text); line-height: 1.5; max-width: 48rem; }",
".ocas-card-header { font-weight: 600; padding: 0.75rem 1rem;" +
" border-bottom: 1px solid var(--ocas-card-border); }",
".ocas-card-body { padding: 1rem; }",
// Hash pill
".ocas-hash { font-family: var(--ocas-mono); font-size: 0.9em;" +
" background: var(--ocas-hash-bg); color: var(--ocas-hash-text);" +
" padding: 0.15em 0.4em; border-radius: 4px; word-break: break-all; }",
// Key-value grid
".ocas-dl { display: grid; grid-template-columns: auto 1fr; gap: 0.25rem 1rem; margin: 0; padding: 1rem; }",
".ocas-dl dt { font-weight: 600; color: var(--ocas-text-muted); }",
".ocas-dl dd { margin: 0; }",
// Tag pills
".ocas-tag { display: inline-block; background: #eff6ff; color: #2563eb;" +
" font-size: 0.85em; padding: 0.1em 0.5em; border-radius: 9999px; margin-right: 0.25rem; }",
// Status badges
".ocas-badge { display: inline-block; padding: 0.25em 0.75em; border-radius: 9999px;" +
" font-weight: 500; font-size: 0.9em; }",
".ocas-badge-ok { background: #dcfce7; color: var(--ocas-green); }",
".ocas-badge-error { background: #fee2e2; color: var(--ocas-red); }",
".ocas-badge-warn { background: #fef3c7; color: var(--ocas-yellow); }",
// Template content code block
".ocas-template-content { white-space: pre-wrap; background: var(--ocas-hash-bg);" +
" font-family: var(--ocas-mono); padding: 0.75rem; border-radius: 4px;" +
" overflow-x: auto; margin: 0; }",
// Stats grid (gc, export, import)
".ocas-stats-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 1rem; }",
".ocas-stat { display: flex; flex-direction: column; }",
".ocas-stat-value { font-size: var(--ocas-metric-size); font-weight: 600;" +
" font-variant-numeric: tabular-nums; }",
".ocas-stat-label { color: var(--ocas-text-muted); font-size: 0.85em; }",
// Semantic highlights
".ocas-success { color: var(--ocas-green); }",
".ocas-zero { color: var(--ocas-text-muted); }",
// Hash list (refs, walk)
".ocas-hash-list { list-style: none; padding: 0; margin: 0; }",
".ocas-hash-list li { padding: 0.25rem 0; }",
// History (var-history)
".ocas-history-values { padding-left: 1.5rem; margin: 0; }",
".ocas-history-values li { padding: 0.25rem 0; }",
".ocas-current-marker { color: var(--ocas-green); font-size: 0.85em; font-weight: 500; }",
// Legacy .ocas-output support (structured/list/stats templates)
".ocas-output { font-family: var(--ocas-font); line-height: 1.5; max-width: 48rem; }",
".ocas-output dl { display: grid; grid-template-columns: auto 1fr; gap: 0.25rem 1rem; margin: 0; }",
".ocas-output dt { font-weight: 600; color: var(--ocas-text-muted); }",
".ocas-output dd { margin: 0; }",
".ocas-output table { border-collapse: collapse; width: 100%; }",
".ocas-output th, .ocas-output td { text-align: left; padding: 0.4rem 0.75rem; border-bottom: 1px solid var(--ocas-card-border); }",
".ocas-output th { font-weight: 600; color: var(--ocas-text-muted); background: #f9fafb; }",
".ocas-output ul { list-style: none; padding: 0; margin: 0; }",
".ocas-output li { padding: 0.2rem 0; }",
".ocas-stats .ocas-metric { font-size: 1.25em; font-weight: 600; }",
".ocas-verify-ok .ocas-status { color: var(--ocas-green); }",
".ocas-verify-corrupted .ocas-status { color: var(--ocas-red); }",
".ocas-verify-invalid .ocas-status { color: var(--ocas-yellow); }",
// Table styles (design-guide-compliant)
".ocas-table { border-collapse: collapse; width: 100%; }",
".ocas-table th { text-align: left; padding: 0.4rem 0.75rem;" +
" text-transform: uppercase; font-size: 0.75em; font-weight: 600;" +
" color: var(--ocas-text-muted); letter-spacing: 0.05em;" +
" border-bottom: 1px solid var(--ocas-card-border); }",
".ocas-table td { text-align: left; padding: 0.4rem 0.75rem;" +
" border-bottom: 1px solid var(--ocas-card-border); }",
".ocas-table tr:last-child td { border-bottom: none; }",
".ocas-col-name { font-weight: 500; }",
".ocas-col-time { color: var(--ocas-text-muted); font-variant-numeric: tabular-nums; }",
].join(" ");
/**
* Register default LiquidJS templates for all @ocas/output/* schemas.
* Each template is stored as a @ocas/string CAS node and bound to
* the variable `@ocas/template/{format}/`.
*
* Registers both text and HTML templates. HTML templates also get a
* shared CSS static template (`@ocas/template-static/html/`).
*
* Idempotent: safe to call multiple times.
*/
export async function registerOutputTemplates(
store: Store,
): Promise> {
const aliases = bootstrap(store);
const stringHash = aliases["@ocas/string"];
if (stringHash === undefined) {
throw new Error("@ocas/string schema not found in bootstrap result");
}
const registered: Record = {};
// Register text templates
for (const [alias, template] of TEXT_TEMPLATES) {
const schemaHash = aliases[alias];
if (schemaHash === undefined) {
throw new Error(`Schema alias not found: ${alias}`);
}
const contentHash = store.cas.put(stringHash, template);
const varName = `@ocas/template/text/${schemaHash}`;
store.var.set(varName, contentHash);
registered[alias] = contentHash;
}
// Register HTML templates
for (const [alias, template] of HTML_TEMPLATES) {
const schemaHash = aliases[alias];
if (schemaHash === undefined) {
throw new Error(`Schema alias not found: ${alias}`);
}
const contentHash = store.cas.put(stringHash, template);
const varName = `@ocas/template/html/${schemaHash}`;
store.var.set(varName, contentHash);
// Register shared CSS static template for this schema
const staticJson = JSON.stringify({ css: OUTPUT_CSS });
const staticHash = store.cas.put(stringHash, staticJson);
const staticVarName = `@ocas/template-static/html/${schemaHash}`;
store.var.set(staticVarName, staticHash);
}
return registered;
}