UNPKG

@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
// ============================================================================ // 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'); };