@xuda.io/runtime-bundle
Version:
The Xuda Runtime Bundle refers to a collection of scripts and libraries packaged together to provide the necessary runtime environment for executing plugins or components in the Xuda platform.
104 lines (92 loc) • 4.22 kB
JavaScript
// ============================================================================
// LLM guide loader — SERVER-ONLY (imports node:fs; never load in the browser).
// Serves the studio authoring guide in runtime/docs/llm/ to external AIs via
// cpi get_studio_guide and as the MCP server's `instructions`. Markdown files
// carry {{PLACEHOLDER}} tokens for every enumerable fact (menu types, xu
// attributes, workflow actions…) which are merged AT SERVE TIME from the live
// registries in xuda-studio-doc-utils.mjs / xuda-studio-checker.mjs — each box
// serves its own deployed truth, hand-typed lists never drift.
// ============================================================================
import fs from 'node:fs/promises';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { valid_menuType } from './xuda-studio-doc-utils.mjs';
import { XU_UI_ATTRIBUTES, XU_ATTRIBUTE_PREFIXES, WORKFLOW_ACTIONS, DEPRECATED_WORKFLOW_ACTIONS, INVOKE_ACTION_VALUES, PROG_EVENT_TYPES, PROTECTED_VAR_NAMES, AUTHORING_TAGS } from './xuda-studio-checker.mjs';
import { XU_SCRIPT_API } from './xuda-api-library.mjs';
// runtime/js/modules/ -> runtime/docs/llm/ (works from any XUDA_HOME).
const DOCS_DIR = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', '..', 'docs', 'llm');
const render_list = (arr) => arr.map((v) => `\`${v}\``).join(', ');
const render_map = (obj) => Object.entries(obj)
.map(([k, v]) => `- \`${k}\` — ${v}`)
.join('\n');
// Static registry placeholders. {{TOPICS}} is resolved per-request (dir scan).
const REGISTRY = {
VALID_MENU_TYPES: render_list(valid_menuType),
XU_ATTRIBUTES: render_map(XU_UI_ATTRIBUTES),
XU_ATTRIBUTE_PREFIXES: render_map(XU_ATTRIBUTE_PREFIXES),
WORKFLOW_ACTIONS: render_map(WORKFLOW_ACTIONS),
DEPRECATED_WORKFLOW_ACTIONS: render_list(DEPRECATED_WORKFLOW_ACTIONS),
INVOKE_ACTION_VALUES: render_list(INVOKE_ACTION_VALUES),
PROG_EVENT_TYPES: render_list(PROG_EVENT_TYPES),
PROTECTED_VARS: render_list(PROTECTED_VAR_NAMES),
AUTHORING_XU_TAGS: render_list(AUTHORING_TAGS),
XU_SCRIPT_API: render_map(XU_SCRIPT_API),
};
// topic -> { mtimeMs, content } (merged). Re-read only when the file changes.
const _cache = new Map();
const _safe_topic = function (topic) {
// Topics are bare names: strip anything path-like so a caller can never
// escape DOCS_DIR.
return String(topic || '').replace(/\.md$/i, '').replace(/[^a-z0-9_-]/gi, '');
};
export const get_topics = async function () {
const files = await fs.readdir(DOCS_DIR);
const topics = [];
for (const f of files.sort()) {
if (!f.endsWith('.md')) continue;
const topic = f.slice(0, -3);
let title = topic;
try {
const head = await fs.readFile(path.join(DOCS_DIR, f), 'utf8');
const m = head.match(/^#\s+(.+)$/m);
if (m) title = m[1].trim();
} catch (e) {}
topics.push({ topic, title });
}
return topics;
};
export const get_guide = async function (topic) {
const name = _safe_topic(topic) || 'instructions';
const file = path.join(DOCS_DIR, name + '.md');
let stat;
try {
stat = await fs.stat(file);
} catch (e) {
const topics = await get_topics().catch(() => []);
const err = new Error(`unknown topic "${name}" - available topics: ${topics.map((t) => t.topic).join(', ')}`);
err.code = 'UNKNOWN_TOPIC';
err.topics = topics;
throw err;
}
const cached = _cache.get(name);
let content;
if (cached && cached.mtimeMs === stat.mtimeMs) {
content = cached.content;
} else {
content = await fs.readFile(file, 'utf8');
for (const [key, val] of Object.entries(REGISTRY)) {
content = content.replaceAll(`{{${key}}}`, val);
}
_cache.set(name, { mtimeMs: stat.mtimeMs, content });
}
// {{TOPICS}} lists the guide's table of contents — resolved per call so a
// newly dropped topic file appears without touching the others.
if (content.includes('{{TOPICS}}')) {
const topics = await get_topics();
content = content.replaceAll('{{TOPICS}}', topics.map((t) => `- \`${t.topic}\` — ${t.title}`).join('\n'));
}
return content;
};
export const get_instructions = async function () {
return get_guide('instructions');
};