Files
agent-ui/server/services/vue-parser.ts
josedario87 d27da30494 feat: Replace DB component tools with filesystem-based user-components/
Components are now .vue files in user-components/<folder>/ parsed at runtime.
Replaces 6 DB MCP tools with 2 (list_fs_components, load_fs_component).
Adds vue-parser, fs-components API, and file watcher for live reload.
2026-02-18 10:24:05 -06:00

118 lines
3.1 KiB
TypeScript

/**
* Vue File Parser
* Extracts <template>, <script setup>, <style> from .vue files
* and converts them into VueComponentDefinition objects.
*/
import { join } from 'path'
import { readdirSync, readFileSync, existsSync, statSync } from 'fs'
export interface VueComponentDefinition {
id: string
name: string
template: string
setup?: string
style?: string
props?: string[]
imports?: string[]
tags?: string[]
folder: string
}
export interface ComponentMeta {
name?: string
tags?: string[]
props?: string[]
imports?: string[]
}
/**
* Parse a .vue file content into template, setup, style sections
*/
export function parseVueFile(content: string): { template: string; setup: string; style: string } {
const templateMatch = content.match(/<template>([\s\S]*?)<\/template>/)
const scriptMatch = content.match(/<script\s+setup[^>]*>([\s\S]*?)<\/script>/)
const styleMatch = content.match(/<style[^>]*>([\s\S]*?)<\/style>/)
return {
template: templateMatch?.[1]?.trim() || '',
setup: scriptMatch?.[1]?.trim() || '',
style: styleMatch?.[1]?.trim() || ''
}
}
/**
* Read optional meta.json from a component folder
*/
export function readMetaJson(folderPath: string): ComponentMeta | null {
const metaPath = join(folderPath, 'meta.json')
if (!existsSync(metaPath)) return null
try {
const raw = readFileSync(metaPath, 'utf-8')
return JSON.parse(raw) as ComponentMeta
} catch {
return null
}
}
/**
* Parse a component folder into a VueComponentDefinition
* Convention: user-components/counter-widget/CounterWidget.vue + optional meta.json
*/
export function parseComponentFolder(baseDir: string, folderName: string): VueComponentDefinition | null {
const folderPath = join(baseDir, folderName)
if (!existsSync(folderPath) || !statSync(folderPath).isDirectory()) return null
// Find the .vue file in the folder
const files = readdirSync(folderPath)
const vueFile = files.find(f => f.endsWith('.vue'))
if (!vueFile) return null
const vueContent = readFileSync(join(folderPath, vueFile), 'utf-8')
const { template, setup, style } = parseVueFile(vueContent)
if (!template) return null
const meta = readMetaJson(folderPath)
// Derive name from meta or filename (CounterWidget.vue -> CounterWidget)
const nameFromFile = vueFile.replace('.vue', '')
const name = meta?.name || nameFromFile
return {
id: folderName,
name,
template,
setup: setup || undefined,
style: style || undefined,
props: meta?.props,
imports: meta?.imports,
tags: meta?.tags,
folder: folderName
}
}
/**
* List all component subdirectories and parse them
*/
export function listAllComponents(baseDir: string): VueComponentDefinition[] {
if (!existsSync(baseDir)) return []
const entries = readdirSync(baseDir)
const components: VueComponentDefinition[] = []
for (const entry of entries) {
const fullPath = join(baseDir, entry)
if (!statSync(fullPath).isDirectory()) continue
const component = parseComponentFolder(baseDir, entry)
if (component) {
components.push(component)
}
}
return components
}