@wot-ui/cli
Advanced tools
Sorry, the diff of this file is not supported yet
| import { a as findComponent, d as resolveVersion, f as loadMetadataFile, i as lintProject, m as version, n as getCliUpdateStatus, o as listComponents, p as name } from "./update-check-cp6uTpce.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.", | ||
| "Use wot_status when the user asks about tool health, updates, or unexpected missing metadata.", | ||
| "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, options = {}) { | ||
| server.registerTool("wot_status", { | ||
| description: "Get wot-ui MCP server and CLI update status.", | ||
| inputSchema: z.object({}), | ||
| annotations: { | ||
| readOnlyHint: true, | ||
| destructiveHint: false, | ||
| idempotentHint: true, | ||
| openWorldHint: true | ||
| } | ||
| }, async () => { | ||
| const update = await getCliUpdateStatus({ | ||
| currentVersion: version, | ||
| packageName: name, | ||
| ...options.updateCheckOptions | ||
| }); | ||
| return { content: [{ | ||
| type: "text", | ||
| text: jsonText({ | ||
| server: { | ||
| name: "wot-ui", | ||
| version | ||
| }, | ||
| cli: update | ||
| }) | ||
| }] }; | ||
| }); | ||
| 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); | ||
| getCliUpdateStatus({ | ||
| currentVersion: version, | ||
| packageName: name | ||
| }).catch(() => {}); | ||
| 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 }; |
| import process from "node:process"; | ||
| import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } 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"; | ||
| import { homedir } from "node:os"; | ||
| //#region package.json | ||
| var name = "@wot-ui/cli"; | ||
| var version = "1.0.3"; | ||
| //#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 | ||
| /** Strip semver range operators (^, ~, >=, >, <=, <, =, whitespace). */ | ||
| function stripRange(ver) { | ||
| return ver.replace(/[\^~>=<\s]/g, ""); | ||
| } | ||
| /** | ||
| * Returns all stable version strings for major key 'v2', | ||
| * sorted ascending by semver. | ||
| */ | ||
| function stableV2Versions() { | ||
| const map = loadVersionsFile().v2 ?? {}; | ||
| return Object.values(map).filter((v) => !v.includes("-")).sort((a, b) => { | ||
| const pa = a.split(".").map(Number); | ||
| const pb = b.split(".").map(Number); | ||
| for (let i = 0; i < 3; i++) { | ||
| const diff = (pa[i] ?? 0) - (pb[i] ?? 0); | ||
| if (diff !== 0) return diff; | ||
| } | ||
| return 0; | ||
| }); | ||
| } | ||
| /** | ||
| * Auto-detect the wot-ui version to use. | ||
| * | ||
| * Priority: | ||
| * 1. --version flag (flagVersion arg) | ||
| * 2. node_modules/@wot-ui/ui/package.json in cwd | ||
| * 3. package.json dependencies[@wot-ui/ui] in cwd | ||
| * 4. Fallback to latest stable version from versions.json | ||
| */ | ||
| function detectVersion(flagVersion, cwd) { | ||
| const dir = cwd ?? process.cwd(); | ||
| if (flagVersion) return { | ||
| version: flagVersion, | ||
| source: "flag" | ||
| }; | ||
| const nmPath = join(dir, "node_modules", "@wot-ui", "ui", "package.json"); | ||
| if (existsSync(nmPath)) try { | ||
| const pkg = JSON.parse(readFileSync(nmPath, "utf8")); | ||
| if (pkg.version) return { | ||
| version: pkg.version, | ||
| source: "node_modules" | ||
| }; | ||
| } catch {} | ||
| const pkgPath = join(dir, "package.json"); | ||
| if (existsSync(pkgPath)) try { | ||
| const pkg = JSON.parse(readFileSync(pkgPath, "utf8")); | ||
| const depVersion = pkg.dependencies?.["@wot-ui/ui"] ?? pkg.devDependencies?.["@wot-ui/ui"] ?? pkg.peerDependencies?.["@wot-ui/ui"]; | ||
| if (depVersion) return { | ||
| version: stripRange(depVersion), | ||
| source: "package.json" | ||
| }; | ||
| } catch {} | ||
| return { | ||
| version: stableV2Versions().at(-1) ?? "2.0.0", | ||
| source: "fallback" | ||
| }; | ||
| } | ||
| /** | ||
| * Resolve a version string (from detectVersion or CLI flag) to a data file key. | ||
| * | ||
| * Examples: | ||
| * undefined / 'v2' → 'v2' (major alias, data/v2.json) | ||
| * 'latest' → 'v2.0.4' (latest stable snapshot) | ||
| * '2.0' → 'v2.0.4' (minor → lookup in versions.json) | ||
| * '2.0.4' → 'v2.0.4' (exact patch) | ||
| * '2.0.0-alpha.5' → 'v2.0.0-alpha.5' (pre-release exact) | ||
| */ | ||
| function resolveVersion(requested) { | ||
| if (!requested || requested === "v2") return "v2"; | ||
| const normalized = requested.trim(); | ||
| if (normalized === "latest") { | ||
| const latest = stableV2Versions().at(-1); | ||
| if (!latest) return "v2"; | ||
| return `v${latest}`; | ||
| } | ||
| const map = loadVersionsFile().v2 ?? {}; | ||
| if (/^\d+\.\d+$/.test(normalized)) { | ||
| const patch = map[normalized]; | ||
| if (!patch) throw new Error(`Unsupported wot-ui version: ${requested}`); | ||
| return `v${patch}`; | ||
| } | ||
| if (/^\d+\.\d+\.\d+/.test(normalized)) { | ||
| if (normalized.split(".")[0] !== "2") throw new Error(`Unsupported wot-ui version: ${requested}`); | ||
| return `v${normalized}`; | ||
| } | ||
| throw new Error(`Unsupported wot-ui version: ${requested}`); | ||
| } | ||
| //#endregion | ||
| //#region src/utils/terminal.ts | ||
| const ANSI = { | ||
| cyan: ["\x1B[36m", "\x1B[39m"], | ||
| dim: ["\x1B[2m", "\x1B[22m"], | ||
| green: ["\x1B[32m", "\x1B[39m"], | ||
| red: ["\x1B[31m", "\x1B[39m"], | ||
| yellow: ["\x1B[33m", "\x1B[39m"] | ||
| }; | ||
| function supportsColor(options = {}) { | ||
| const env = options.env ?? process.env; | ||
| if (!(options.isTty ?? process.stderr.isTTY)) return false; | ||
| if ("NO_COLOR" in env || env.FORCE_COLOR === "0" || env.TERM === "dumb") return false; | ||
| return true; | ||
| } | ||
| function writeStderrLine(message) { | ||
| process.stderr.write(`${message}\n`); | ||
| } | ||
| function formatLogMessage(level, message, options = {}) { | ||
| const color = createColorizer(options); | ||
| return `${color.dim("[wot]")} ${styleLevel(level, message, color)}`; | ||
| } | ||
| function formatStatusLabel(status, options = {}) { | ||
| const normalized = status.toUpperCase(); | ||
| const color = createColorizer(options); | ||
| if (status === "ok" || status === "pass") return color.green(normalized); | ||
| if (status === "warn" || status === "warning") return color.yellow(normalized); | ||
| return color.red(normalized); | ||
| } | ||
| function formatCommand(command, options = {}) { | ||
| return createColorizer(options).cyan(command); | ||
| } | ||
| function formatUpdateNotice(status, options = {}) { | ||
| const color = createColorizer(options); | ||
| const currentVersion = color.dim(status.currentVersion); | ||
| const latestVersion = color.green(status.latestVersion ?? "unknown"); | ||
| return [ | ||
| formatLogMessage("update", "Update available", options), | ||
| `${color.dim("[wot]")} ${status.packageName} ${currentVersion} -> ${latestVersion}`, | ||
| `${color.dim("[wot]")} Run: ${formatCommand(status.command, options)}` | ||
| ].join("\n"); | ||
| } | ||
| function createColorizer(options) { | ||
| const enabled = supportsColor(options); | ||
| return { | ||
| cyan: (value) => applyAnsi(value, ANSI.cyan, enabled), | ||
| dim: (value) => applyAnsi(value, ANSI.dim, enabled), | ||
| green: (value) => applyAnsi(value, ANSI.green, enabled), | ||
| red: (value) => applyAnsi(value, ANSI.red, enabled), | ||
| yellow: (value) => applyAnsi(value, ANSI.yellow, enabled) | ||
| }; | ||
| } | ||
| function styleLevel(level, message, color) { | ||
| if (level === "error") return color.red(message); | ||
| if (level === "success") return color.green(message); | ||
| if (level === "warn" || level === "update") return color.yellow(message); | ||
| if (level === "hint") return color.cyan(message); | ||
| return message; | ||
| } | ||
| function applyAnsi(value, code, enabled) { | ||
| return enabled ? `${code[0]}${value}${code[1]}` : value; | ||
| } | ||
| //#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$1, version$1) { | ||
| const normalized = name$1.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 | ||
| //#region src/utils/update-check.ts | ||
| const DEFAULT_CHECK_INTERVAL_MS = 1440 * 60 * 1e3; | ||
| const DEFAULT_TIMEOUT_MS = 1500; | ||
| const DEFAULT_REGISTRY = "https://registry.npmjs.org"; | ||
| function compareSemver(a, b) { | ||
| const parsedA = parseSemver(a); | ||
| const parsedB = parseSemver(b); | ||
| if (!parsedA || !parsedB) return 0; | ||
| for (const index of [ | ||
| 0, | ||
| 1, | ||
| 2 | ||
| ]) { | ||
| const diff = parsedA[index] - parsedB[index]; | ||
| if (diff !== 0) return diff > 0 ? 1 : -1; | ||
| } | ||
| return comparePrerelease(parsedA[3], parsedB[3]); | ||
| } | ||
| function shouldCheckForCliUpdate(args = process.argv, env = process.env, isTty = process.stderr.isTTY) { | ||
| if (!isTty) return false; | ||
| if (isUpdateCheckDisabled(env) || isTruthyEnv(env.CI) || env.NODE_ENV === "test") return false; | ||
| const userArgs = args.slice(2); | ||
| if (userArgs.some((arg) => arg === "-V" || arg === "-h" || arg === "--help")) return false; | ||
| const command = userArgs.find((arg) => !arg.startsWith("-")); | ||
| return command !== "mcp" && command !== "help"; | ||
| } | ||
| function checkForCliUpdate(options) { | ||
| const env = options.env ?? process.env; | ||
| const args = options.args ?? process.argv; | ||
| const stderr = options.stderr ?? process.stderr; | ||
| const isTty = options.isTty ?? process.stderr.isTTY; | ||
| if (!shouldCheckForCliUpdate(args, env, isTty)) return; | ||
| try { | ||
| const status = getCachedCliUpdateStatus(options); | ||
| if (status.updateAvailable && status.latestVersion) stderr.write(`${formatUpdateNotice(status, { | ||
| env, | ||
| isTty | ||
| })}\n`); | ||
| } catch {} | ||
| } | ||
| function getCachedCliUpdateStatus(options) { | ||
| const env = options.env ?? process.env; | ||
| const baseStatus = createBaseStatus(options, env); | ||
| if (baseStatus.disabled) return { | ||
| ...baseStatus, | ||
| cached: false, | ||
| updateAvailable: false | ||
| }; | ||
| const now = options.now ?? Date.now(); | ||
| const cached = readCache(options.cacheFile ?? getDefaultCacheFile(env)); | ||
| const intervalMs = options.checkIntervalMs ?? DEFAULT_CHECK_INTERVAL_MS; | ||
| const cacheIsFresh = !!cached && now - cached.checkedAt < intervalMs; | ||
| const latestVersion = cacheIsFresh ? cached.latestVersion : void 0; | ||
| return { | ||
| ...baseStatus, | ||
| cached: cacheIsFresh, | ||
| checkedAt: cacheIsFresh ? cached.checkedAt : void 0, | ||
| latestVersion, | ||
| updateAvailable: !!latestVersion && compareSemver(latestVersion, options.currentVersion) > 0 | ||
| }; | ||
| } | ||
| async function getCliUpdateStatus(options) { | ||
| const env = options.env ?? process.env; | ||
| const baseStatus = createBaseStatus(options, env); | ||
| if (baseStatus.disabled) return { | ||
| ...baseStatus, | ||
| cached: false, | ||
| updateAvailable: false | ||
| }; | ||
| const now = options.now ?? Date.now(); | ||
| const cacheFile = options.cacheFile ?? getDefaultCacheFile(env); | ||
| const cached = readCache(cacheFile); | ||
| const intervalMs = options.checkIntervalMs ?? DEFAULT_CHECK_INTERVAL_MS; | ||
| const cacheIsFresh = !!cached && now - cached.checkedAt < intervalMs; | ||
| const result = cacheIsFresh ? cached : await fetchAndCacheLatestVersion(options, cacheFile, now); | ||
| const latestVersion = result.latestVersion; | ||
| return { | ||
| ...baseStatus, | ||
| cached: cacheIsFresh, | ||
| checkedAt: result.checkedAt, | ||
| latestVersion, | ||
| updateAvailable: !!latestVersion && compareSemver(latestVersion, options.currentVersion) > 0 | ||
| }; | ||
| } | ||
| function createBaseStatus(options, env) { | ||
| return { | ||
| command: `npm install -g ${options.packageName}`, | ||
| currentVersion: options.currentVersion, | ||
| disabled: isUpdateCheckDisabled(env), | ||
| packageName: options.packageName | ||
| }; | ||
| } | ||
| function parseSemver(version$1) { | ||
| const match = version$1.trim().replace(/^v/, "").match(/^(\d+)\.(\d+)\.(\d+)(?:-([^+]+))?(?:\+.*)?$/); | ||
| if (!match) return void 0; | ||
| return [ | ||
| Number(match[1]), | ||
| Number(match[2]), | ||
| Number(match[3]), | ||
| match[4] | ||
| ]; | ||
| } | ||
| function comparePrerelease(a, b) { | ||
| if (!a && !b) return 0; | ||
| if (!a) return 1; | ||
| if (!b) return -1; | ||
| const identifiersA = a.split("."); | ||
| const identifiersB = b.split("."); | ||
| const length = Math.max(identifiersA.length, identifiersB.length); | ||
| for (let index = 0; index < length; index++) { | ||
| const identifierA = identifiersA[index]; | ||
| const identifierB = identifiersB[index]; | ||
| if (identifierA === void 0) return -1; | ||
| if (identifierB === void 0) return 1; | ||
| if (identifierA === identifierB) continue; | ||
| const numberA = parseNumericIdentifier(identifierA); | ||
| const numberB = parseNumericIdentifier(identifierB); | ||
| if (numberA !== void 0 && numberB !== void 0) return numberA > numberB ? 1 : -1; | ||
| if (numberA !== void 0) return -1; | ||
| if (numberB !== void 0) return 1; | ||
| return identifierA > identifierB ? 1 : -1; | ||
| } | ||
| return 0; | ||
| } | ||
| function parseNumericIdentifier(identifier) { | ||
| if (!/^(?:0|[1-9]\d*)$/.test(identifier)) return void 0; | ||
| return Number(identifier); | ||
| } | ||
| function isTruthyEnv(value) { | ||
| return !!value && value !== "0" && value !== "false"; | ||
| } | ||
| function isUpdateCheckDisabled(env) { | ||
| return isTruthyEnv(env.WOT_DISABLE_UPDATE_CHECK) || isTruthyEnv(env.NO_UPDATE_NOTIFIER); | ||
| } | ||
| function getDefaultCacheFile(env) { | ||
| return join(env.XDG_CACHE_HOME ? join(env.XDG_CACHE_HOME, "open-wot") : join(homedir(), ".cache", "open-wot"), "update-check.json"); | ||
| } | ||
| function readCache(cacheFile) { | ||
| if (!existsSync(cacheFile)) return void 0; | ||
| let cache; | ||
| try { | ||
| cache = JSON.parse(readFileSync(cacheFile, "utf8")); | ||
| } catch { | ||
| return; | ||
| } | ||
| if (!cache || typeof cache !== "object" || typeof cache.checkedAt !== "number") return void 0; | ||
| return { | ||
| checkedAt: cache.checkedAt, | ||
| latestVersion: typeof cache.latestVersion === "string" ? cache.latestVersion : void 0 | ||
| }; | ||
| } | ||
| async function fetchAndCacheLatestVersion(options, cacheFile, now) { | ||
| let latestVersion; | ||
| try { | ||
| latestVersion = await fetchLatestVersion(options.packageName, options.registry ?? options.env?.npm_config_registry ?? DEFAULT_REGISTRY, options.fetchFn, options.timeoutMs ?? DEFAULT_TIMEOUT_MS); | ||
| } catch { | ||
| latestVersion = void 0; | ||
| } | ||
| const cache = { | ||
| checkedAt: now, | ||
| latestVersion | ||
| }; | ||
| writeCache(cacheFile, cache); | ||
| return cache; | ||
| } | ||
| async function fetchLatestVersion(packageName, registry, fetchFn, timeoutMs) { | ||
| const request = fetchFn ?? globalThis.fetch; | ||
| if (typeof request !== "function") return void 0; | ||
| const controller = new AbortController(); | ||
| const timeout = setTimeout(() => controller.abort(), timeoutMs); | ||
| try { | ||
| const response = await request(`${registry.replace(/\/+$/, "")}/${encodePackageName(packageName)}/latest`, { | ||
| headers: { | ||
| "accept": "application/json", | ||
| "user-agent": `${packageName} update-check` | ||
| }, | ||
| signal: controller.signal | ||
| }); | ||
| if (!response.ok) return void 0; | ||
| const json = await response.json(); | ||
| if (isRegistryLatestResponse(json)) return json.version; | ||
| } finally { | ||
| clearTimeout(timeout); | ||
| } | ||
| } | ||
| function encodePackageName(packageName) { | ||
| if (!packageName.startsWith("@")) return encodeURIComponent(packageName); | ||
| const [scope, name$1] = packageName.split("/"); | ||
| return `${scope}%2f${name$1}`; | ||
| } | ||
| function isRegistryLatestResponse(value) { | ||
| return typeof value === "object" && value !== null && "version" in value && typeof value.version === "string"; | ||
| } | ||
| function writeCache(cacheFile, cache) { | ||
| try { | ||
| mkdirSync(dirname(cacheFile), { recursive: true }); | ||
| writeFileSync(cacheFile, `${JSON.stringify(cache, null, 2)}\n`); | ||
| } catch {} | ||
| } | ||
| //#endregion | ||
| export { findComponent as a, formatStatusLabel as c, resolveVersion as d, loadMetadataFile as f, lintProject as i, writeStderrLine as l, version as m, getCliUpdateStatus as n, listComponents as o, name as p, analyzeUsage as r, formatLogMessage as s, checkForCliUpdate as t, detectVersion as u }; |
@@ -12,4 +12,5 @@ { | ||
| "2.0": "2.0.8", | ||
| "2.1": "2.1.0" | ||
| "2.1": "2.1.0", | ||
| "2.2": "2.2.0" | ||
| } | ||
| } |
+16
-11
| #!/usr/bin/env node | ||
| import { a as detectVersion, c as version, i as listComponents, n as lintProject, o as resolveVersion, r as findComponent, s as loadMetadataFile, t as analyzeUsage } from "./scanner-CO2cUuK4.mjs"; | ||
| import { a as findComponent, c as formatStatusLabel, d as resolveVersion, f as loadMetadataFile, i as lintProject, l as writeStderrLine, m as version, o as listComponents, p as name, r as analyzeUsage, s as formatLogMessage, t as checkForCliUpdate, u as detectVersion } from "./update-check-cp6uTpce.mjs"; | ||
| import process from "node:process"; | ||
@@ -27,3 +27,3 @@ import { Command } from "commander"; | ||
| const info = detectVersion(typeof options.version === "string" ? options.version : void 0); | ||
| if (info.source === "fallback") process.stderr.write(`[wot] version not detected in project, falling back to ${info.version}\n`); | ||
| if (info.source === "fallback") writeStderrLine(formatLogMessage("warn", `Version not detected in project, falling back to ${info.version}`)); | ||
| return { | ||
@@ -39,3 +39,3 @@ format, | ||
| }); | ||
| else console.error(message); | ||
| else console.error(formatLogMessage("error", message)); | ||
| } | ||
@@ -169,4 +169,4 @@ function getComponentLabel(component) { | ||
| } | ||
| function getDependencyVersion(pkg, name) { | ||
| return pkg?.dependencies?.[name] ?? pkg?.devDependencies?.[name]; | ||
| function getDependencyVersion(pkg, name$1) { | ||
| return pkg?.dependencies?.[name$1] ?? pkg?.devDependencies?.[name$1]; | ||
| } | ||
@@ -177,5 +177,5 @@ function detectWotDependency(pkg) { | ||
| } | ||
| function createCheck(name, status, message, suggestion) { | ||
| function createCheck(name$1, status, message, suggestion) { | ||
| if (status === "pass") return { | ||
| name, | ||
| name: name$1, | ||
| status, | ||
@@ -185,3 +185,3 @@ message | ||
| return { | ||
| name, | ||
| name: name$1, | ||
| status, | ||
@@ -236,3 +236,3 @@ severity: status === "fail" ? "error" : "warning", | ||
| } | ||
| writeLine([`Directory: ${report.dir}`, ...report.checks.map((check) => `${check.status.toUpperCase()} ${check.name}: ${check.message}${check.suggestion ? ` | ${check.suggestion}` : ""}`)].join("\n")); | ||
| writeLine([`Directory: ${report.dir}`, ...report.checks.map((check) => `${formatStatusLabel(check.status)} ${check.name}: ${check.message}${check.suggestion ? ` | ${check.suggestion}` : ""}`)].join("\n")); | ||
| } catch (error) { | ||
@@ -302,3 +302,3 @@ printError(error instanceof Error ? error.message : "Failed to diagnose project", query.format); | ||
| } | ||
| writeLine([`Scanned files: ${report.scannedFiles}`, ...report.issues.map((issue) => `${issue.severity.toUpperCase()} ${issue.file}:${issue.line} [${issue.rule}] ${issue.message}`)].join("\n")); | ||
| writeLine([`Scanned files: ${report.scannedFiles}`, ...report.issues.map((issue) => `${formatStatusLabel(issue.severity)} ${issue.file}:${issue.line} [${issue.rule}] ${issue.message}`)].join("\n")); | ||
| } catch (error) { | ||
@@ -334,3 +334,3 @@ printError(error instanceof Error ? error.message : "Failed to lint project", query.format); | ||
| program.command("mcp").description("Start the wot-ui MCP server").action(async () => { | ||
| const { startMcpServer } = await import("./server-CjiMp3bA.mjs"); | ||
| const { startMcpServer } = await import("./server-CbSmyhFy.mjs"); | ||
| await startMcpServer(); | ||
@@ -422,2 +422,7 @@ }); | ||
| //#region src/index.ts | ||
| checkForCliUpdate({ | ||
| args: process.argv, | ||
| currentVersion: version, | ||
| packageName: name | ||
| }); | ||
| await createCliProgram().parseAsync(process.argv); | ||
@@ -424,0 +429,0 @@ |
+1
-1
| { | ||
| "name": "@wot-ui/cli", | ||
| "type": "module", | ||
| "version": "1.0.2", | ||
| "version": "1.0.3", | ||
| "description": "面向 wot-ui 的 CLI、MCP 与数据提取工具集", | ||
@@ -6,0 +6,0 @@ "license": "MIT", |
+14
-7
@@ -27,2 +27,4 @@ # Open Wot | ||
| `wot` 在交互式终端启动时会自动检查 `@wot-ui/cli` 是否有新版本。检查结果最多缓存 24 小时,提示只写入 stderr,不会污染 `--format json` 的 stdout;CI 和非交互式环境会自动跳过。`wot mcp` 不会在启动时输出更新提示,MCP 客户端可通过 `wot_status` tool 查询 CLI 更新状态。若需要关闭检查,可设置 `WOT_DISABLE_UPDATE_CHECK=1` 或 `NO_UPDATE_NOTIFIER=1`。 | ||
| 如果你在仓库内本地调试,推荐直接运行源码入口,而不是依赖全局命令: | ||
@@ -102,10 +104,15 @@ | ||
| - `wot_list` | ||
| - `wot_info` | ||
| - `wot_doc` | ||
| - `wot_demo` | ||
| - `wot_token` | ||
| - `wot_changelog` | ||
| - `wot_lint` | ||
| | Tool | 功能 | 主要参数 | | ||
| | --- | --- | --- | | ||
| | `wot_status` | 查看 MCP Server 与 `@wot-ui/cli` 状态,包括当前版本、是否有 CLI 更新及更新命令。 | 无 | | ||
| | `wot_list` | 列出当前离线知识库中的 wot-ui 组件元数据,适合在生成页面前发现可用组件。 | `version` | | ||
| | `wot_info` | 查询单个组件的 props、events、slots、CSS 变量等结构化信息。 | `component`, `version` | | ||
| | `wot_doc` | 获取单个组件的完整 markdown 文档,适合需要阅读用法细节或限制说明时调用。 | `component`, `version` | | ||
| | `wot_demo` | 获取组件 demo 列表,或按 demo 名称获取指定示例源码。 | `component`, `demo`, `version` | | ||
| | `wot_token` | 查询组件 CSS 变量;不传组件名时返回所有组件的 CSS 变量摘要。 | `component`, `version` | | ||
| | `wot_changelog` | 查询 wot-ui v2 离线数据中的更新记录,可按版本或组件过滤。 | `version`, `component` | | ||
| | `wot_lint` | 扫描本地项目中的 wot-ui 使用问题,例如未知组件、空按钮等规则。 | `dir`, `version` | | ||
| 其中 `version` 支持与 CLI 一致的写法,例如 `2.0`、`2.0.4`、`latest`;不传时会按项目依赖或离线数据自动解析。 | ||
| ## 数据来源 | ||
@@ -112,0 +119,0 @@ |
| import process from "node:process"; | ||
| 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 = "1.0.2"; | ||
| //#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 | ||
| /** Strip semver range operators (^, ~, >=, >, <=, <, =, whitespace). */ | ||
| function stripRange(ver) { | ||
| return ver.replace(/[\^~>=<\s]/g, ""); | ||
| } | ||
| /** | ||
| * Returns all stable version strings for major key 'v2', | ||
| * sorted ascending by semver. | ||
| */ | ||
| function stableV2Versions() { | ||
| const map = loadVersionsFile().v2 ?? {}; | ||
| return Object.values(map).filter((v) => !v.includes("-")).sort((a, b) => { | ||
| const pa = a.split(".").map(Number); | ||
| const pb = b.split(".").map(Number); | ||
| for (let i = 0; i < 3; i++) { | ||
| const diff = (pa[i] ?? 0) - (pb[i] ?? 0); | ||
| if (diff !== 0) return diff; | ||
| } | ||
| return 0; | ||
| }); | ||
| } | ||
| /** | ||
| * Auto-detect the wot-ui version to use. | ||
| * | ||
| * Priority: | ||
| * 1. --version flag (flagVersion arg) | ||
| * 2. node_modules/@wot-ui/ui/package.json in cwd | ||
| * 3. package.json dependencies[@wot-ui/ui] in cwd | ||
| * 4. Fallback to latest stable version from versions.json | ||
| */ | ||
| function detectVersion(flagVersion, cwd) { | ||
| const dir = cwd ?? process.cwd(); | ||
| if (flagVersion) return { | ||
| version: flagVersion, | ||
| source: "flag" | ||
| }; | ||
| const nmPath = join(dir, "node_modules", "@wot-ui", "ui", "package.json"); | ||
| if (existsSync(nmPath)) try { | ||
| const pkg = JSON.parse(readFileSync(nmPath, "utf8")); | ||
| if (pkg.version) return { | ||
| version: pkg.version, | ||
| source: "node_modules" | ||
| }; | ||
| } catch {} | ||
| const pkgPath = join(dir, "package.json"); | ||
| if (existsSync(pkgPath)) try { | ||
| const pkg = JSON.parse(readFileSync(pkgPath, "utf8")); | ||
| const depVersion = pkg.dependencies?.["@wot-ui/ui"] ?? pkg.devDependencies?.["@wot-ui/ui"] ?? pkg.peerDependencies?.["@wot-ui/ui"]; | ||
| if (depVersion) return { | ||
| version: stripRange(depVersion), | ||
| source: "package.json" | ||
| }; | ||
| } catch {} | ||
| return { | ||
| version: stableV2Versions().at(-1) ?? "2.0.0", | ||
| source: "fallback" | ||
| }; | ||
| } | ||
| /** | ||
| * Resolve a version string (from detectVersion or CLI flag) to a data file key. | ||
| * | ||
| * Examples: | ||
| * undefined / 'v2' → 'v2' (major alias, data/v2.json) | ||
| * 'latest' → 'v2.0.4' (latest stable snapshot) | ||
| * '2.0' → 'v2.0.4' (minor → lookup in versions.json) | ||
| * '2.0.4' → 'v2.0.4' (exact patch) | ||
| * '2.0.0-alpha.5' → 'v2.0.0-alpha.5' (pre-release exact) | ||
| */ | ||
| function resolveVersion(requested) { | ||
| if (!requested || requested === "v2") return "v2"; | ||
| const normalized = requested.trim(); | ||
| if (normalized === "latest") { | ||
| const latest = stableV2Versions().at(-1); | ||
| if (!latest) return "v2"; | ||
| return `v${latest}`; | ||
| } | ||
| const map = loadVersionsFile().v2 ?? {}; | ||
| if (/^\d+\.\d+$/.test(normalized)) { | ||
| const patch = map[normalized]; | ||
| if (!patch) throw new Error(`Unsupported wot-ui version: ${requested}`); | ||
| return `v${patch}`; | ||
| } | ||
| if (/^\d+\.\d+\.\d+/.test(normalized)) { | ||
| if (normalized.split(".")[0] !== "2") throw new Error(`Unsupported wot-ui version: ${requested}`); | ||
| return `v${normalized}`; | ||
| } | ||
| 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 { detectVersion as a, version as c, listComponents as i, lintProject as n, resolveVersion as o, findComponent as r, loadMetadataFile as s, analyzeUsage as t }; |
| import { c as version, i as listComponents, n as lintProject, o as resolveVersion, r as findComponent, s as loadMetadataFile } from "./scanner-CO2cUuK4.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 }; |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Network access
Supply chain riskThis module accesses the network.
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
2314500
9.61%21
5%1232
32.47%224
3.23%9
200%2
100%