@wot-ui/cli
Advanced tools
| import { existsSync, readFileSync, readdirSync } from "node:fs"; | ||
| import { dirname, join, relative, resolve } from "node:path"; | ||
| import { fileURLToPath } from "node:url"; | ||
| import { gunzipSync } from "node:zlib"; | ||
| import { parse } from "@vue/compiler-sfc"; | ||
| //#region package.json | ||
| var version = "0.0.1-beta.8"; | ||
| //#endregion | ||
| //#region src/data/loader.ts | ||
| const currentDir = dirname(fileURLToPath(import.meta.url)); | ||
| function resolveDataDir() { | ||
| const candidates = [ | ||
| join(currentDir, "..", "data"), | ||
| join(currentDir, "..", "..", "data"), | ||
| join(currentDir, "data") | ||
| ]; | ||
| for (const candidate of candidates) if (existsSync(join(candidate, "versions.json")) || existsSync(join(candidate, "versions.json.gz"))) return candidate; | ||
| throw new Error("Unable to locate bundled data directory"); | ||
| } | ||
| const dataDir = resolveDataDir(); | ||
| function readJsonFile(baseName) { | ||
| const jsonPath = join(dataDir, `${baseName}.json`); | ||
| if (existsSync(jsonPath)) return JSON.parse(readFileSync(jsonPath, "utf8")); | ||
| const gzipPath = join(dataDir, `${baseName}.json.gz`); | ||
| if (existsSync(gzipPath)) { | ||
| const compressed = readFileSync(gzipPath); | ||
| return JSON.parse(gunzipSync(compressed).toString("utf8")); | ||
| } | ||
| throw new Error(`Data file not found for ${baseName}`); | ||
| } | ||
| function loadVersionsFile() { | ||
| return readJsonFile("versions"); | ||
| } | ||
| function loadMetadataFile(versionKey) { | ||
| return readJsonFile(versionKey); | ||
| } | ||
| //#endregion | ||
| //#region src/data/version.ts | ||
| const VERSION_KEY = "v2"; | ||
| function resolveVersion(requested) { | ||
| const versions = loadVersionsFile(); | ||
| if (!requested) return VERSION_KEY; | ||
| const normalized = requested.trim(); | ||
| if (versions.aliases[normalized]) return VERSION_KEY; | ||
| if (versions.supported.includes(normalized)) return VERSION_KEY; | ||
| if (normalized === VERSION_KEY) return VERSION_KEY; | ||
| throw new Error(`Unsupported wot-ui version: ${requested}`); | ||
| } | ||
| //#endregion | ||
| //#region src/data/metadata.ts | ||
| function loadResolvedMetadata(version$1) { | ||
| return loadMetadataFile(resolveVersion(version$1)); | ||
| } | ||
| function listComponents(version$1) { | ||
| return loadResolvedMetadata(version$1).components; | ||
| } | ||
| function findComponent(name, version$1) { | ||
| const normalized = name.trim().toLowerCase(); | ||
| return listComponents(version$1).find((component) => component.name.toLowerCase() === normalized || component.tag.toLowerCase() === normalized); | ||
| } | ||
| //#endregion | ||
| //#region src/utils/files.ts | ||
| const DEFAULT_IGNORES = new Set([ | ||
| ".git", | ||
| ".idea", | ||
| ".output", | ||
| ".turbo", | ||
| ".vscode", | ||
| "dist", | ||
| "build", | ||
| "coverage", | ||
| "node_modules" | ||
| ]); | ||
| function walkFiles(rootDir, extensions) { | ||
| const results = []; | ||
| function visit(dir) { | ||
| for (const entry of readdirSync(dir, { withFileTypes: true })) { | ||
| if (DEFAULT_IGNORES.has(entry.name)) continue; | ||
| const fullPath = join(dir, entry.name); | ||
| if (entry.isDirectory()) { | ||
| visit(fullPath); | ||
| continue; | ||
| } | ||
| if (extensions.some((extension) => entry.name.endsWith(extension))) results.push(fullPath); | ||
| } | ||
| } | ||
| visit(rootDir); | ||
| return results; | ||
| } | ||
| function safeRelative(rootDir, filePath) { | ||
| return relative(rootDir, filePath) || "."; | ||
| } | ||
| //#endregion | ||
| //#region src/utils/scanner.ts | ||
| const IMPORT_RE = /from\s+['"]([^'"]*wot[^'"]*)['"]/g; | ||
| const TAG_RE = /<\s*(wd-[a-z0-9-]+)/gi; | ||
| const BUTTON_RE = /<wd-button\b([^>]*)>([\s\S]*?)<\/wd-button>|<wd-button\b([^>]*)\/>/gi; | ||
| function getLineNumber(source, index) { | ||
| return source.slice(0, index).split("\n").length; | ||
| } | ||
| function collectTemplateTags(content) { | ||
| const counts = /* @__PURE__ */ new Map(); | ||
| for (const match of content.matchAll(TAG_RE)) { | ||
| const tag = match[1]?.toLowerCase(); | ||
| if (!tag) continue; | ||
| counts.set(tag, (counts.get(tag) ?? 0) + 1); | ||
| } | ||
| return counts; | ||
| } | ||
| function collectImports(scriptContent) { | ||
| const imports = /* @__PURE__ */ new Set(); | ||
| for (const match of scriptContent.matchAll(IMPORT_RE)) if (match[1]) imports.add(match[1]); | ||
| return [...imports]; | ||
| } | ||
| function analyzeUsage(targetDir, version$1) { | ||
| const dir = resolve(targetDir); | ||
| const files = walkFiles(dir, [".vue"]); | ||
| const knownByTag = new Map(listComponents(version$1).map((component) => [component.tag.toLowerCase(), component])); | ||
| const usageMap = /* @__PURE__ */ new Map(); | ||
| const imports = /* @__PURE__ */ new Set(); | ||
| for (const file of files) { | ||
| const parsed = parse(readFileSync(file, "utf8"), { filename: file }); | ||
| const template = parsed.descriptor.template?.content ?? ""; | ||
| const script = [parsed.descriptor.script?.content ?? "", parsed.descriptor.scriptSetup?.content ?? ""].filter(Boolean).join("\n"); | ||
| for (const item of collectImports(script)) imports.add(item); | ||
| for (const [tag, count] of collectTemplateTags(template)) { | ||
| const known = knownByTag.get(tag); | ||
| const key = known?.name ?? tag; | ||
| const existing = usageMap.get(key); | ||
| if (existing) { | ||
| existing.count += count; | ||
| if (!existing.files.includes(safeRelative(dir, file))) existing.files.push(safeRelative(dir, file)); | ||
| continue; | ||
| } | ||
| usageMap.set(key, { | ||
| name: known?.name ?? tag, | ||
| tag, | ||
| count, | ||
| files: [safeRelative(dir, file)] | ||
| }); | ||
| } | ||
| } | ||
| return { | ||
| scannedFiles: files.length, | ||
| components: [...usageMap.values()].sort((left, right) => right.count - left.count || left.name.localeCompare(right.name)), | ||
| imports: [...imports].sort() | ||
| }; | ||
| } | ||
| function lintProject(targetDir, version$1) { | ||
| const dir = resolve(targetDir); | ||
| const files = walkFiles(dir, [".vue"]); | ||
| const issues = []; | ||
| for (const file of files) { | ||
| const template = parse(readFileSync(file, "utf8"), { filename: file }).descriptor.template?.content ?? ""; | ||
| for (const match of template.matchAll(TAG_RE)) { | ||
| const tag = match[1]?.toLowerCase(); | ||
| if (!tag) continue; | ||
| if (!findComponent(tag, version$1)) issues.push({ | ||
| file: safeRelative(dir, file), | ||
| line: getLineNumber(template, match.index ?? 0), | ||
| rule: "unknown-component", | ||
| severity: "warning", | ||
| message: `Unknown wot-ui component tag: ${tag}` | ||
| }); | ||
| } | ||
| for (const match of template.matchAll(BUTTON_RE)) { | ||
| const attrs = (match[1] ?? match[3] ?? "").trim(); | ||
| const body = (match[2] ?? "").replace(/<[^>]+>/g, "").trim(); | ||
| if (!/\bicon\s*=/.test(attrs) && !body) issues.push({ | ||
| file: safeRelative(dir, file), | ||
| line: getLineNumber(template, match.index ?? 0), | ||
| rule: "button-content", | ||
| severity: "warning", | ||
| message: "wd-button should include visible text content or an icon attribute." | ||
| }); | ||
| const component = findComponent("wd-button", version$1); | ||
| for (const prop of component?.props ?? []) { | ||
| if (!prop.deprecated) continue; | ||
| if (!(/* @__PURE__ */ new RegExp(`\\b${prop.name}\\b`)).test(attrs)) continue; | ||
| issues.push({ | ||
| file: safeRelative(dir, file), | ||
| line: getLineNumber(template, match.index ?? 0), | ||
| rule: "deprecated-prop", | ||
| severity: "warning", | ||
| message: prop.replacement ? `Deprecated prop ${prop.name} detected on wd-button. Use ${prop.replacement} instead.` : `Deprecated prop ${prop.name} detected on wd-button.` | ||
| }); | ||
| } | ||
| } | ||
| } | ||
| return { | ||
| scannedFiles: files.length, | ||
| issues | ||
| }; | ||
| } | ||
| //#endregion | ||
| export { resolveVersion as a, listComponents as i, lintProject as n, loadMetadataFile as o, findComponent as r, version as s, analyzeUsage as t }; |
| import { a as resolveVersion, i as listComponents, n as lintProject, o as loadMetadataFile, r as findComponent, s as version } from "./scanner-DYthnCXr.mjs"; | ||
| import process from "node:process"; | ||
| import { McpServer, StdioServerTransport } from "@modelcontextprotocol/server"; | ||
| import * as z from "zod/v4"; | ||
| //#region src/mcp/prompts.ts | ||
| const WOT_EXPERT_PROMPT = [ | ||
| "You are a wot-ui expert assistant.", | ||
| "Always query component metadata before generating code.", | ||
| "Prefer using wot_list, wot_info, wot_doc, and wot_token before writing UI code.", | ||
| "Assume only wot-ui v2 is supported by this server." | ||
| ].join(" "); | ||
| const WOT_PAGE_GENERATOR_PROMPT = [ | ||
| "Generate wot-ui pages by first collecting every relevant component API and CSS variable.", | ||
| "Prefer existing wd-* components and documented props over ad-hoc custom markup.", | ||
| "When theme customization is involved, inspect CSS variables with wot_token first." | ||
| ].join(" "); | ||
| //#endregion | ||
| //#region src/mcp/tools.ts | ||
| function jsonText(value) { | ||
| return JSON.stringify(value, null, 2); | ||
| } | ||
| function registerMcpTools(server) { | ||
| server.registerTool("wot_list", { | ||
| description: "List available wot-ui components.", | ||
| inputSchema: z.object({ version: z.string().optional() }), | ||
| annotations: { | ||
| readOnlyHint: true, | ||
| destructiveHint: false, | ||
| idempotentHint: true, | ||
| openWorldHint: false | ||
| } | ||
| }, async ({ version: version$1 }) => { | ||
| return { content: [{ | ||
| type: "text", | ||
| text: jsonText({ components: listComponents(version$1) }) | ||
| }] }; | ||
| }); | ||
| server.registerTool("wot_info", { | ||
| description: "Get props, events, slots, and CSS variables for a component.", | ||
| inputSchema: z.object({ | ||
| component: z.string(), | ||
| version: z.string().optional() | ||
| }), | ||
| annotations: { | ||
| readOnlyHint: true, | ||
| destructiveHint: false, | ||
| idempotentHint: true, | ||
| openWorldHint: false | ||
| } | ||
| }, async ({ component, version: version$1 }) => { | ||
| const result = findComponent(component, version$1); | ||
| if (!result) return { | ||
| isError: true, | ||
| content: [{ | ||
| type: "text", | ||
| text: `Component not found: ${component}` | ||
| }] | ||
| }; | ||
| return { content: [{ | ||
| type: "text", | ||
| text: jsonText(result) | ||
| }] }; | ||
| }); | ||
| server.registerTool("wot_doc", { | ||
| description: "Get component markdown documentation.", | ||
| inputSchema: z.object({ | ||
| component: z.string(), | ||
| version: z.string().optional() | ||
| }), | ||
| annotations: { | ||
| readOnlyHint: true, | ||
| destructiveHint: false, | ||
| idempotentHint: true, | ||
| openWorldHint: false | ||
| } | ||
| }, async ({ component, version: version$1 }) => { | ||
| const result = findComponent(component, version$1); | ||
| if (!result?.doc) return { | ||
| isError: true, | ||
| content: [{ | ||
| type: "text", | ||
| text: `Documentation not found: ${component}` | ||
| }] | ||
| }; | ||
| return { content: [{ | ||
| type: "text", | ||
| text: result.doc | ||
| }] }; | ||
| }); | ||
| server.registerTool("wot_demo", { | ||
| description: "Get component demo code or list demos.", | ||
| inputSchema: z.object({ | ||
| component: z.string(), | ||
| demo: z.string().optional(), | ||
| version: z.string().optional() | ||
| }), | ||
| annotations: { | ||
| readOnlyHint: true, | ||
| destructiveHint: false, | ||
| idempotentHint: true, | ||
| openWorldHint: false | ||
| } | ||
| }, async ({ component, demo, version: version$1 }) => { | ||
| const result = findComponent(component, version$1); | ||
| if (!result) return { | ||
| isError: true, | ||
| content: [{ | ||
| type: "text", | ||
| text: `Component not found: ${component}` | ||
| }] | ||
| }; | ||
| if (!demo) return { content: [{ | ||
| type: "text", | ||
| text: jsonText({ demos: result.demos ?? [] }) | ||
| }] }; | ||
| const matched = result.demos?.find((item) => item.name.toLowerCase() === demo.toLowerCase()); | ||
| if (!matched) return { | ||
| isError: true, | ||
| content: [{ | ||
| type: "text", | ||
| text: `Demo not found: ${demo}` | ||
| }] | ||
| }; | ||
| return { content: [{ | ||
| type: "text", | ||
| text: jsonText(matched) | ||
| }] }; | ||
| }); | ||
| server.registerTool("wot_token", { | ||
| description: "Get component CSS variables.", | ||
| inputSchema: z.object({ | ||
| component: z.string().optional(), | ||
| version: z.string().optional() | ||
| }), | ||
| annotations: { | ||
| readOnlyHint: true, | ||
| destructiveHint: false, | ||
| idempotentHint: true, | ||
| openWorldHint: false | ||
| } | ||
| }, async ({ component, version: version$1 }) => { | ||
| if (!component) return { content: [{ | ||
| type: "text", | ||
| text: jsonText({ components: listComponents(version$1).map((item) => ({ | ||
| name: item.name, | ||
| cssVars: item.cssVars | ||
| })) }) | ||
| }] }; | ||
| const result = findComponent(component, version$1); | ||
| if (!result) return { | ||
| isError: true, | ||
| content: [{ | ||
| type: "text", | ||
| text: `Component not found: ${component}` | ||
| }] | ||
| }; | ||
| return { content: [{ | ||
| type: "text", | ||
| text: jsonText({ | ||
| name: result.name, | ||
| cssVars: result.cssVars | ||
| }) | ||
| }] }; | ||
| }); | ||
| server.registerTool("wot_changelog", { | ||
| description: "Get changelog entries for the supported v2 dataset.", | ||
| inputSchema: z.object({ | ||
| version: z.string().optional(), | ||
| component: z.string().optional() | ||
| }), | ||
| annotations: { | ||
| readOnlyHint: true, | ||
| destructiveHint: false, | ||
| idempotentHint: true, | ||
| openWorldHint: false | ||
| } | ||
| }, async ({ version: version$1, component }) => { | ||
| return { content: [{ | ||
| type: "text", | ||
| text: jsonText({ entries: (loadMetadataFile(resolveVersion(version$1)).changelog ?? []).filter((entry) => { | ||
| const versionMatches = version$1 ? entry.version === version$1 || `v${entry.version}` === version$1 : true; | ||
| const componentMatches = component ? (entry.components ?? []).some((item) => item.toLowerCase() === component.toLowerCase()) : true; | ||
| return versionMatches && componentMatches; | ||
| }) }) | ||
| }] }; | ||
| }); | ||
| server.registerTool("wot_lint", { | ||
| description: "Lint a local project for wot-ui related issues.", | ||
| inputSchema: z.object({ | ||
| dir: z.string().optional(), | ||
| version: z.string().optional() | ||
| }), | ||
| annotations: { | ||
| readOnlyHint: true, | ||
| destructiveHint: false, | ||
| idempotentHint: true, | ||
| openWorldHint: true | ||
| } | ||
| }, async ({ dir, version: version$1 }) => { | ||
| return { content: [{ | ||
| type: "text", | ||
| text: jsonText(lintProject(dir ?? process.cwd(), version$1)) | ||
| }] }; | ||
| }); | ||
| } | ||
| //#endregion | ||
| //#region src/mcp/server.ts | ||
| async function startMcpServer() { | ||
| const server = new McpServer({ | ||
| name: "wot-ui", | ||
| version | ||
| }, { | ||
| instructions: "Use wot-ui component tools before generating UI code. Only wot-ui v2 metadata is available in this server.", | ||
| capabilities: { logging: {} } | ||
| }); | ||
| registerMcpTools(server); | ||
| server.registerPrompt("wot-expert", { description: "General wot-ui expert workflow." }, async () => ({ messages: [{ | ||
| role: "assistant", | ||
| content: { | ||
| type: "text", | ||
| text: WOT_EXPERT_PROMPT | ||
| } | ||
| }] })); | ||
| server.registerPrompt("wot-page-generator", { | ||
| description: "Workflow for generating a wot-ui page.", | ||
| argsSchema: z.object({ goal: z.string().optional() }) | ||
| }, async ({ goal }) => ({ messages: [{ | ||
| role: "assistant", | ||
| content: { | ||
| type: "text", | ||
| text: goal ? `${WOT_PAGE_GENERATOR_PROMPT} Goal: ${goal}` : WOT_PAGE_GENERATOR_PROMPT | ||
| } | ||
| }] })); | ||
| const transport = new StdioServerTransport(); | ||
| await server.connect(transport); | ||
| const shutdown = async () => { | ||
| await server.close(); | ||
| process.exit(0); | ||
| }; | ||
| process.on("SIGINT", shutdown); | ||
| process.on("SIGTERM", shutdown); | ||
| } | ||
| //#endregion | ||
| export { startMcpServer }; |
+2
-2
| #!/usr/bin/env node | ||
| import { a as resolveVersion, i as listComponents, n as lintProject, o as loadMetadataFile, r as findComponent, s as version, t as analyzeUsage } from "./scanner-B5dljsZu.mjs"; | ||
| import { a as resolveVersion, i as listComponents, n as lintProject, o as loadMetadataFile, r as findComponent, s as version, t as analyzeUsage } from "./scanner-DYthnCXr.mjs"; | ||
| import process from "node:process"; | ||
@@ -331,3 +331,3 @@ import { Command } from "commander"; | ||
| program.command("mcp").description("Start the wot-ui MCP server").action(async () => { | ||
| const { startMcpServer } = await import("./server-Bn2u7Czs.mjs"); | ||
| const { startMcpServer } = await import("./server-65ejz4L2.mjs"); | ||
| await startMcpServer(); | ||
@@ -334,0 +334,0 @@ }); |
+5
-4
| { | ||
| "name": "@wot-ui/cli", | ||
| "type": "module", | ||
| "version": "0.0.1-beta.7", | ||
| "version": "0.0.1-beta.8", | ||
| "description": "面向 wot-ui 的 CLI、MCP 与数据提取工具集", | ||
@@ -45,2 +45,4 @@ "license": "MIT", | ||
| "@types/node": "^25.0.1", | ||
| "@vitest/coverage-v8": "4.0.15", | ||
| "baseline-browser-mapping": "^2.10.18", | ||
| "bumpp": "^10.3.2", | ||
@@ -63,3 +65,3 @@ "eslint": "^9.39.2", | ||
| "lint-staged": { | ||
| "*": "eslint --fix" | ||
| "*": "eslint --fix --no-warn-ignored" | ||
| }, | ||
@@ -76,4 +78,3 @@ "scripts": { | ||
| "test": "vitest run", | ||
| "test:all": "pnpm test", | ||
| "test:cli": "pnpm test", | ||
| "test:coverage": "vitest run --coverage --coverage.reporter=lcov --coverage.reporter=text", | ||
| "test:watch": "vitest", | ||
@@ -80,0 +81,0 @@ "typecheck": "tsc --noEmit", |
+1
-1
@@ -1,2 +0,2 @@ | ||
| # open-wot | ||
| # Open Wot | ||
@@ -3,0 +3,0 @@ open-wot 是 wot-ui 的 AI 工具链仓库,当前对外发布的核心包为 `@wot-ui/cli`。它提供命令行工具、MCP Server、离线组件知识库与数据提取脚本,用于把 wot-ui v2 的组件知识接入编辑器、AI Agent 和本地工程分析流程。 |
| import { existsSync, readFileSync, readdirSync } from "node:fs"; | ||
| import { dirname, join, relative, resolve } from "node:path"; | ||
| import { fileURLToPath } from "node:url"; | ||
| import { gunzipSync } from "node:zlib"; | ||
| import { parse } from "@vue/compiler-sfc"; | ||
| //#region package.json | ||
| var version = "0.0.1-beta.7"; | ||
| //#endregion | ||
| //#region src/data/loader.ts | ||
| const currentDir = dirname(fileURLToPath(import.meta.url)); | ||
| function resolveDataDir() { | ||
| const candidates = [ | ||
| join(currentDir, "..", "data"), | ||
| join(currentDir, "..", "..", "data"), | ||
| join(currentDir, "data") | ||
| ]; | ||
| for (const candidate of candidates) if (existsSync(join(candidate, "versions.json")) || existsSync(join(candidate, "versions.json.gz"))) return candidate; | ||
| throw new Error("Unable to locate bundled data directory"); | ||
| } | ||
| const dataDir = resolveDataDir(); | ||
| function readJsonFile(baseName) { | ||
| const jsonPath = join(dataDir, `${baseName}.json`); | ||
| if (existsSync(jsonPath)) return JSON.parse(readFileSync(jsonPath, "utf8")); | ||
| const gzipPath = join(dataDir, `${baseName}.json.gz`); | ||
| if (existsSync(gzipPath)) { | ||
| const compressed = readFileSync(gzipPath); | ||
| return JSON.parse(gunzipSync(compressed).toString("utf8")); | ||
| } | ||
| throw new Error(`Data file not found for ${baseName}`); | ||
| } | ||
| function loadVersionsFile() { | ||
| return readJsonFile("versions"); | ||
| } | ||
| function loadMetadataFile(versionKey) { | ||
| return readJsonFile(versionKey); | ||
| } | ||
| //#endregion | ||
| //#region src/data/version.ts | ||
| const VERSION_KEY = "v2"; | ||
| function resolveVersion(requested) { | ||
| const versions = loadVersionsFile(); | ||
| if (!requested) return VERSION_KEY; | ||
| const normalized = requested.trim(); | ||
| if (versions.aliases[normalized]) return VERSION_KEY; | ||
| if (versions.supported.includes(normalized)) return VERSION_KEY; | ||
| if (normalized === VERSION_KEY) return VERSION_KEY; | ||
| throw new Error(`Unsupported wot-ui version: ${requested}`); | ||
| } | ||
| //#endregion | ||
| //#region src/data/metadata.ts | ||
| function loadResolvedMetadata(version$1) { | ||
| return loadMetadataFile(resolveVersion(version$1)); | ||
| } | ||
| function listComponents(version$1) { | ||
| return loadResolvedMetadata(version$1).components; | ||
| } | ||
| function findComponent(name, version$1) { | ||
| const normalized = name.trim().toLowerCase(); | ||
| return listComponents(version$1).find((component) => component.name.toLowerCase() === normalized || component.tag.toLowerCase() === normalized); | ||
| } | ||
| //#endregion | ||
| //#region src/utils/files.ts | ||
| const DEFAULT_IGNORES = new Set([ | ||
| ".git", | ||
| ".idea", | ||
| ".output", | ||
| ".turbo", | ||
| ".vscode", | ||
| "dist", | ||
| "build", | ||
| "coverage", | ||
| "node_modules" | ||
| ]); | ||
| function walkFiles(rootDir, extensions) { | ||
| const results = []; | ||
| function visit(dir) { | ||
| for (const entry of readdirSync(dir, { withFileTypes: true })) { | ||
| if (DEFAULT_IGNORES.has(entry.name)) continue; | ||
| const fullPath = join(dir, entry.name); | ||
| if (entry.isDirectory()) { | ||
| visit(fullPath); | ||
| continue; | ||
| } | ||
| if (extensions.some((extension) => entry.name.endsWith(extension))) results.push(fullPath); | ||
| } | ||
| } | ||
| visit(rootDir); | ||
| return results; | ||
| } | ||
| function safeRelative(rootDir, filePath) { | ||
| return relative(rootDir, filePath) || "."; | ||
| } | ||
| //#endregion | ||
| //#region src/utils/scanner.ts | ||
| const IMPORT_RE = /from\s+['"]([^'"]*wot[^'"]*)['"]/g; | ||
| const TAG_RE = /<\s*(wd-[a-z0-9-]+)/gi; | ||
| const BUTTON_RE = /<wd-button\b([^>]*)>([\s\S]*?)<\/wd-button>|<wd-button\b([^>]*)\/>/gi; | ||
| function getLineNumber(source, index) { | ||
| return source.slice(0, index).split("\n").length; | ||
| } | ||
| function collectTemplateTags(content) { | ||
| const counts = /* @__PURE__ */ new Map(); | ||
| for (const match of content.matchAll(TAG_RE)) { | ||
| const tag = match[1]?.toLowerCase(); | ||
| if (!tag) continue; | ||
| counts.set(tag, (counts.get(tag) ?? 0) + 1); | ||
| } | ||
| return counts; | ||
| } | ||
| function collectImports(scriptContent) { | ||
| const imports = /* @__PURE__ */ new Set(); | ||
| for (const match of scriptContent.matchAll(IMPORT_RE)) if (match[1]) imports.add(match[1]); | ||
| return [...imports]; | ||
| } | ||
| function analyzeUsage(targetDir, version$1) { | ||
| const dir = resolve(targetDir); | ||
| const files = walkFiles(dir, [".vue"]); | ||
| const knownByTag = new Map(listComponents(version$1).map((component) => [component.tag.toLowerCase(), component])); | ||
| const usageMap = /* @__PURE__ */ new Map(); | ||
| const imports = /* @__PURE__ */ new Set(); | ||
| for (const file of files) { | ||
| const parsed = parse(readFileSync(file, "utf8"), { filename: file }); | ||
| const template = parsed.descriptor.template?.content ?? ""; | ||
| const script = [parsed.descriptor.script?.content ?? "", parsed.descriptor.scriptSetup?.content ?? ""].filter(Boolean).join("\n"); | ||
| for (const item of collectImports(script)) imports.add(item); | ||
| for (const [tag, count] of collectTemplateTags(template)) { | ||
| const known = knownByTag.get(tag); | ||
| const key = known?.name ?? tag; | ||
| const existing = usageMap.get(key); | ||
| if (existing) { | ||
| existing.count += count; | ||
| if (!existing.files.includes(safeRelative(dir, file))) existing.files.push(safeRelative(dir, file)); | ||
| continue; | ||
| } | ||
| usageMap.set(key, { | ||
| name: known?.name ?? tag, | ||
| tag, | ||
| count, | ||
| files: [safeRelative(dir, file)] | ||
| }); | ||
| } | ||
| } | ||
| return { | ||
| scannedFiles: files.length, | ||
| components: [...usageMap.values()].sort((left, right) => right.count - left.count || left.name.localeCompare(right.name)), | ||
| imports: [...imports].sort() | ||
| }; | ||
| } | ||
| function lintProject(targetDir, version$1) { | ||
| const dir = resolve(targetDir); | ||
| const files = walkFiles(dir, [".vue"]); | ||
| const issues = []; | ||
| for (const file of files) { | ||
| const template = parse(readFileSync(file, "utf8"), { filename: file }).descriptor.template?.content ?? ""; | ||
| for (const match of template.matchAll(TAG_RE)) { | ||
| const tag = match[1]?.toLowerCase(); | ||
| if (!tag) continue; | ||
| if (!findComponent(tag, version$1)) issues.push({ | ||
| file: safeRelative(dir, file), | ||
| line: getLineNumber(template, match.index ?? 0), | ||
| rule: "unknown-component", | ||
| severity: "warning", | ||
| message: `Unknown wot-ui component tag: ${tag}` | ||
| }); | ||
| } | ||
| for (const match of template.matchAll(BUTTON_RE)) { | ||
| const attrs = (match[1] ?? match[3] ?? "").trim(); | ||
| const body = (match[2] ?? "").replace(/<[^>]+>/g, "").trim(); | ||
| if (!/\bicon\s*=/.test(attrs) && !body) issues.push({ | ||
| file: safeRelative(dir, file), | ||
| line: getLineNumber(template, match.index ?? 0), | ||
| rule: "button-content", | ||
| severity: "warning", | ||
| message: "wd-button should include visible text content or an icon attribute." | ||
| }); | ||
| const component = findComponent("wd-button", version$1); | ||
| for (const prop of component?.props ?? []) { | ||
| if (!prop.deprecated) continue; | ||
| if (!(/* @__PURE__ */ new RegExp(`\\b${prop.name}\\b`)).test(attrs)) continue; | ||
| issues.push({ | ||
| file: safeRelative(dir, file), | ||
| line: getLineNumber(template, match.index ?? 0), | ||
| rule: "deprecated-prop", | ||
| severity: "warning", | ||
| message: prop.replacement ? `Deprecated prop ${prop.name} detected on wd-button. Use ${prop.replacement} instead.` : `Deprecated prop ${prop.name} detected on wd-button.` | ||
| }); | ||
| } | ||
| } | ||
| } | ||
| return { | ||
| scannedFiles: files.length, | ||
| issues | ||
| }; | ||
| } | ||
| //#endregion | ||
| export { resolveVersion as a, listComponents as i, lintProject as n, loadMetadataFile as o, findComponent as r, version as s, analyzeUsage as t }; |
| import { a as resolveVersion, i as listComponents, n as lintProject, o as loadMetadataFile, r as findComponent, s as version } from "./scanner-B5dljsZu.mjs"; | ||
| import process from "node:process"; | ||
| import { McpServer, StdioServerTransport } from "@modelcontextprotocol/server"; | ||
| import * as z from "zod/v4"; | ||
| //#region src/mcp/prompts.ts | ||
| const WOT_EXPERT_PROMPT = [ | ||
| "You are a wot-ui expert assistant.", | ||
| "Always query component metadata before generating code.", | ||
| "Prefer using wot_list, wot_info, wot_doc, and wot_token before writing UI code.", | ||
| "Assume only wot-ui v2 is supported by this server." | ||
| ].join(" "); | ||
| const WOT_PAGE_GENERATOR_PROMPT = [ | ||
| "Generate wot-ui pages by first collecting every relevant component API and CSS variable.", | ||
| "Prefer existing wd-* components and documented props over ad-hoc custom markup.", | ||
| "When theme customization is involved, inspect CSS variables with wot_token first." | ||
| ].join(" "); | ||
| //#endregion | ||
| //#region src/mcp/tools.ts | ||
| function jsonText(value) { | ||
| return JSON.stringify(value, null, 2); | ||
| } | ||
| function registerMcpTools(server) { | ||
| server.registerTool("wot_list", { | ||
| description: "List available wot-ui components.", | ||
| inputSchema: z.object({ version: z.string().optional() }), | ||
| annotations: { | ||
| readOnlyHint: true, | ||
| destructiveHint: false, | ||
| idempotentHint: true, | ||
| openWorldHint: false | ||
| } | ||
| }, async ({ version: version$1 }) => { | ||
| return { content: [{ | ||
| type: "text", | ||
| text: jsonText({ components: listComponents(version$1) }) | ||
| }] }; | ||
| }); | ||
| server.registerTool("wot_info", { | ||
| description: "Get props, events, slots, and CSS variables for a component.", | ||
| inputSchema: z.object({ | ||
| component: z.string(), | ||
| version: z.string().optional() | ||
| }), | ||
| annotations: { | ||
| readOnlyHint: true, | ||
| destructiveHint: false, | ||
| idempotentHint: true, | ||
| openWorldHint: false | ||
| } | ||
| }, async ({ component, version: version$1 }) => { | ||
| const result = findComponent(component, version$1); | ||
| if (!result) return { | ||
| isError: true, | ||
| content: [{ | ||
| type: "text", | ||
| text: `Component not found: ${component}` | ||
| }] | ||
| }; | ||
| return { content: [{ | ||
| type: "text", | ||
| text: jsonText(result) | ||
| }] }; | ||
| }); | ||
| server.registerTool("wot_doc", { | ||
| description: "Get component markdown documentation.", | ||
| inputSchema: z.object({ | ||
| component: z.string(), | ||
| version: z.string().optional() | ||
| }), | ||
| annotations: { | ||
| readOnlyHint: true, | ||
| destructiveHint: false, | ||
| idempotentHint: true, | ||
| openWorldHint: false | ||
| } | ||
| }, async ({ component, version: version$1 }) => { | ||
| const result = findComponent(component, version$1); | ||
| if (!result?.doc) return { | ||
| isError: true, | ||
| content: [{ | ||
| type: "text", | ||
| text: `Documentation not found: ${component}` | ||
| }] | ||
| }; | ||
| return { content: [{ | ||
| type: "text", | ||
| text: result.doc | ||
| }] }; | ||
| }); | ||
| server.registerTool("wot_demo", { | ||
| description: "Get component demo code or list demos.", | ||
| inputSchema: z.object({ | ||
| component: z.string(), | ||
| demo: z.string().optional(), | ||
| version: z.string().optional() | ||
| }), | ||
| annotations: { | ||
| readOnlyHint: true, | ||
| destructiveHint: false, | ||
| idempotentHint: true, | ||
| openWorldHint: false | ||
| } | ||
| }, async ({ component, demo, version: version$1 }) => { | ||
| const result = findComponent(component, version$1); | ||
| if (!result) return { | ||
| isError: true, | ||
| content: [{ | ||
| type: "text", | ||
| text: `Component not found: ${component}` | ||
| }] | ||
| }; | ||
| if (!demo) return { content: [{ | ||
| type: "text", | ||
| text: jsonText({ demos: result.demos ?? [] }) | ||
| }] }; | ||
| const matched = result.demos?.find((item) => item.name.toLowerCase() === demo.toLowerCase()); | ||
| if (!matched) return { | ||
| isError: true, | ||
| content: [{ | ||
| type: "text", | ||
| text: `Demo not found: ${demo}` | ||
| }] | ||
| }; | ||
| return { content: [{ | ||
| type: "text", | ||
| text: jsonText(matched) | ||
| }] }; | ||
| }); | ||
| server.registerTool("wot_token", { | ||
| description: "Get component CSS variables.", | ||
| inputSchema: z.object({ | ||
| component: z.string().optional(), | ||
| version: z.string().optional() | ||
| }), | ||
| annotations: { | ||
| readOnlyHint: true, | ||
| destructiveHint: false, | ||
| idempotentHint: true, | ||
| openWorldHint: false | ||
| } | ||
| }, async ({ component, version: version$1 }) => { | ||
| if (!component) return { content: [{ | ||
| type: "text", | ||
| text: jsonText({ components: listComponents(version$1).map((item) => ({ | ||
| name: item.name, | ||
| cssVars: item.cssVars | ||
| })) }) | ||
| }] }; | ||
| const result = findComponent(component, version$1); | ||
| if (!result) return { | ||
| isError: true, | ||
| content: [{ | ||
| type: "text", | ||
| text: `Component not found: ${component}` | ||
| }] | ||
| }; | ||
| return { content: [{ | ||
| type: "text", | ||
| text: jsonText({ | ||
| name: result.name, | ||
| cssVars: result.cssVars | ||
| }) | ||
| }] }; | ||
| }); | ||
| server.registerTool("wot_changelog", { | ||
| description: "Get changelog entries for the supported v2 dataset.", | ||
| inputSchema: z.object({ | ||
| version: z.string().optional(), | ||
| component: z.string().optional() | ||
| }), | ||
| annotations: { | ||
| readOnlyHint: true, | ||
| destructiveHint: false, | ||
| idempotentHint: true, | ||
| openWorldHint: false | ||
| } | ||
| }, async ({ version: version$1, component }) => { | ||
| return { content: [{ | ||
| type: "text", | ||
| text: jsonText({ entries: (loadMetadataFile(resolveVersion(version$1)).changelog ?? []).filter((entry) => { | ||
| const versionMatches = version$1 ? entry.version === version$1 || `v${entry.version}` === version$1 : true; | ||
| const componentMatches = component ? (entry.components ?? []).some((item) => item.toLowerCase() === component.toLowerCase()) : true; | ||
| return versionMatches && componentMatches; | ||
| }) }) | ||
| }] }; | ||
| }); | ||
| server.registerTool("wot_lint", { | ||
| description: "Lint a local project for wot-ui related issues.", | ||
| inputSchema: z.object({ | ||
| dir: z.string().optional(), | ||
| version: z.string().optional() | ||
| }), | ||
| annotations: { | ||
| readOnlyHint: true, | ||
| destructiveHint: false, | ||
| idempotentHint: true, | ||
| openWorldHint: true | ||
| } | ||
| }, async ({ dir, version: version$1 }) => { | ||
| return { content: [{ | ||
| type: "text", | ||
| text: jsonText(lintProject(dir ?? process.cwd(), version$1)) | ||
| }] }; | ||
| }); | ||
| } | ||
| //#endregion | ||
| //#region src/mcp/server.ts | ||
| async function startMcpServer() { | ||
| const server = new McpServer({ | ||
| name: "wot-ui", | ||
| version | ||
| }, { | ||
| instructions: "Use wot-ui component tools before generating UI code. Only wot-ui v2 metadata is available in this server.", | ||
| capabilities: { logging: {} } | ||
| }); | ||
| registerMcpTools(server); | ||
| server.registerPrompt("wot-expert", { description: "General wot-ui expert workflow." }, async () => ({ messages: [{ | ||
| role: "assistant", | ||
| content: { | ||
| type: "text", | ||
| text: WOT_EXPERT_PROMPT | ||
| } | ||
| }] })); | ||
| server.registerPrompt("wot-page-generator", { | ||
| description: "Workflow for generating a wot-ui page.", | ||
| argsSchema: z.object({ goal: z.string().optional() }) | ||
| }, async ({ goal }) => ({ messages: [{ | ||
| role: "assistant", | ||
| content: { | ||
| type: "text", | ||
| text: goal ? `${WOT_PAGE_GENERATOR_PROMPT} Goal: ${goal}` : WOT_PAGE_GENERATOR_PROMPT | ||
| } | ||
| }] })); | ||
| const transport = new StdioServerTransport(); | ||
| await server.connect(transport); | ||
| const shutdown = async () => { | ||
| await server.close(); | ||
| process.exit(0); | ||
| }; | ||
| process.on("SIGINT", shutdown); | ||
| process.on("SIGTERM", shutdown); | ||
| } | ||
| //#endregion | ||
| export { startMcpServer }; |
1281276
0.01%18
12.5%