@wot-ui/cli
Advanced tools
| 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-DlG1c3ZB.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.4"; | ||
| //#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 }; |
+2
-2
| #!/usr/bin/env node | ||
| 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 { 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-DlG1c3ZB.mjs"; | ||
| import process from "node:process"; | ||
@@ -327,3 +327,3 @@ import { Command } from "commander"; | ||
| program.command("mcp").description("Start the wot-ui MCP server").action(async () => { | ||
| const { startMcpServer } = await import("./server-CbSmyhFy.mjs"); | ||
| const { startMcpServer } = await import("./server-Cbs8eIrc.mjs"); | ||
| await startMcpServer(); | ||
@@ -330,0 +330,0 @@ }); |
+2
-2
| { | ||
| "name": "@wot-ui/cli", | ||
| "type": "module", | ||
| "version": "1.0.3", | ||
| "version": "1.0.4", | ||
| "description": "面向 wot-ui 的 CLI、MCP 与数据提取工具集", | ||
@@ -35,3 +35,3 @@ "license": "MIT", | ||
| "@cfworker/json-schema": "^4.1.1", | ||
| "@modelcontextprotocol/server": "^2.0.0-alpha.2", | ||
| "@modelcontextprotocol/server": "2.0.0-alpha.2", | ||
| "@vue/compiler-sfc": "^3.5.13", | ||
@@ -38,0 +38,0 @@ "commander": "^14.0.0", |
| 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 }; |
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.
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.
2314499
0+ Added
- Removed
- Removed