@wot-ui/cli
Advanced tools
| import { _ as version, c as toComponentSummary, g as name, h as loadMetadataFile, i as lintProject, l as toDemoSummary, m as resolveVersion, n as getCliUpdateStatus, o as findComponent, s as listComponents } from "./update-check-BacAuhBM.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 compactJsonText(value) { | ||
| return JSON.stringify(value); | ||
| } | ||
| 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: compactJsonText({ components: listComponents(version$1).map(toComponentSummary) }) | ||
| }] }; | ||
| }); | ||
| 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 ?? []).map(toDemoSummary) }) | ||
| }] }; | ||
| 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 { homedir } from "node:os"; | ||
| import { gunzipSync } from "node:zlib"; | ||
| import { parse } from "@vue/compiler-sfc"; | ||
| //#region package.json | ||
| var name = "@wot-ui/cli"; | ||
| var version = "1.0.5"; | ||
| //#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 filterComponents(components, keyword) { | ||
| const normalized = keyword?.trim().toLowerCase(); | ||
| if (!normalized) return components; | ||
| return components.filter((component) => { | ||
| return [ | ||
| component.name, | ||
| component.nameZh, | ||
| component.tag, | ||
| component.category, | ||
| component.description, | ||
| component.descriptionZh | ||
| ].some((value) => value.toLowerCase().includes(normalized)); | ||
| }); | ||
| } | ||
| function toComponentSummary(component) { | ||
| return { | ||
| name: component.name, | ||
| nameZh: component.nameZh, | ||
| tag: component.tag, | ||
| category: component.category, | ||
| description: component.descriptionZh || component.description, | ||
| since: component.since | ||
| }; | ||
| } | ||
| function toDemoSummary(demo) { | ||
| return { | ||
| name: demo.name, | ||
| title: demo.title, | ||
| description: demo.description | ||
| }; | ||
| } | ||
| 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 { version as _, filterComponents as a, toComponentSummary as c, formatStatusLabel as d, writeStderrLine as f, name as g, loadMetadataFile as h, lintProject as i, toDemoSummary as l, resolveVersion as m, getCliUpdateStatus as n, findComponent as o, detectVersion as p, analyzeUsage as r, listComponents as s, checkForCliUpdate as t, formatLogMessage as u }; |
+5
-1
| { | ||
| "name": "@wot-ui/cli", | ||
| "type": "module", | ||
| "version": "1.0.5-beta.1", | ||
| "version": "1.0.5", | ||
| "description": "面向 wot-ui 的 CLI、MCP 与数据提取工具集", | ||
@@ -73,2 +73,6 @@ "license": "MIT", | ||
| "dev": "tsdown --watch", | ||
| "site:dev": "pnpm --filter @open-wot/website dev", | ||
| "site:build": "pnpm --filter @open-wot/website build", | ||
| "site:build:next": "pnpm --filter @open-wot/website build:next", | ||
| "site:lint": "pnpm --filter @open-wot/website lint", | ||
| "extract": "tsx scripts/extract.ts", | ||
@@ -75,0 +79,0 @@ "extract:clone": "rm -rf /tmp/open-wot-wot-ui && git clone --depth 1 https://github.com/wot-ui/wot-ui.git /tmp/open-wot-wot-ui && pnpm extract --wot-dir /tmp/open-wot-wot-ui --output data/v2.json", |
+288
-195
@@ -1,118 +0,126 @@ | ||
| # Open Wot | ||
| <p align="center"> | ||
| <a href="https://wot-ui.cn"> | ||
| <img src="./docs/assets/wot-ui-logo.svg" width="112" alt="Wot UI"> | ||
| </a> | ||
| </p> | ||
| open-wot 是 wot-ui 的 AI 工具链仓库,当前对外发布的核心包为 `@wot-ui/cli`。它提供命令行工具、MCP Server、离线组件知识库与数据提取脚本,用于把 wot-ui v2 的组件知识接入编辑器、AI Agent 和本地工程分析流程。 | ||
| <h1 align="center">@wot-ui/cli</h1> | ||
| ## 仓库定位 | ||
| <p align="center"><strong>让 AI 真正懂 wot-ui。</strong></p> | ||
| - 面向 wot-ui v2 的组件知识查询工具 | ||
| - 面向本地项目的组件使用分析与 lint 工具 | ||
| - 面向 AI 客户端的 MCP stdio 服务 | ||
| - 面向仓库维护者的数据提取与同步工作流 | ||
| <p align="center"> | ||
| 把组件 API、文档、示例和版本知识,接入你的终端与 AI 编程工具。 | ||
| </p> | ||
| ## 核心能力 | ||
| <p align="center"> | ||
| <a href="https://www.npmjs.com/package/@wot-ui/cli"><img src="https://img.shields.io/npm/v/%40wot-ui%2Fcli?style=flat-square&color=1c64fd" alt="npm version"></a> | ||
| <a href="https://www.npmjs.com/package/@wot-ui/cli"><img src="https://img.shields.io/npm/dm/%40wot-ui%2Fcli?style=flat-square&color=12b886" alt="npm downloads"></a> | ||
| <a href="https://github.com/wot-ui/open-wot/actions/workflows/ci.yml"><img src="https://img.shields.io/github/actions/workflow/status/wot-ui/open-wot/ci.yml?branch=main&style=flat-square&label=CI" alt="CI"></a> | ||
| <a href="./LICENSE.md"><img src="https://img.shields.io/npm/l/%40wot-ui%2Fcli?style=flat-square" alt="license"></a> | ||
| </p> | ||
| - 组件知识查询:`list`、`info`、`doc`、`demo`、`token`、`changelog` | ||
| - 项目分析:`doctor`、`usage`、`lint` | ||
| - Agent 接入:`wot agent init` 自动配置 MCP、内置 Skill 与 Agent Instructions | ||
| - MCP 生命周期:`wot mcp`(默认启动 Server)、`wot mcp serve/list/init/status/doctor/remove/print` | ||
| - 元数据提取:从 `wot-ui/wot-ui` 源码生成本地 `v2.json` | ||
| <p align="center"> | ||
| <a href="https://cli.wot-ui.cn">官方网站</a> | ||
| · <a href="#-30-秒接入">快速开始</a> | ||
| · <a href="#给-ai-使用推荐">Agent 接入</a> | ||
| · <a href="#在终端使用">CLI</a> | ||
| · <a href="#只配置-mcp">MCP</a> | ||
| · <a href="./CONTRIBUTING.md">参与贡献</a> | ||
| </p> | ||
| ## 安装 | ||
| --- | ||
| 推荐全局安装 CLI: | ||
| 组件文档不应该只能被人阅读。Open Wot 将 wot-ui v2 的组件知识打包成离线数据,并通过 CLI、MCP 和 Skills 提供给开发者与 AI Agent。 | ||
| - **不猜 API**:查询真实的 props、events、slots、CSS 变量和 demo 源码。 | ||
| - **版本对得上**:自动识别项目依赖,也可以精确查询指定的 wot-ui 版本。 | ||
| - **知识离线可用**:组件数据随 npm 包发布,无需文档 API 或密钥。 | ||
| - **配置安全可控**:支持 dry-run、幂等写入、原子更新和失败保护。 | ||
| ## 🚀 30 秒接入 | ||
| 需要 Node.js `>= 20`。 | ||
| ```bash | ||
| npm install -g @wot-ui/cli@latest | ||
| wot agent init --client cursor | ||
| wot agent doctor --client cursor | ||
| ``` | ||
| 也可以使用其他包管理器: | ||
| 完成后,当前项目会获得: | ||
| ```bash | ||
| pnpm add -g @wot-ui/cli@latest | ||
| bun add -g @wot-ui/cli@latest | ||
| ```text | ||
| MCP Server AI 可以按需调用 8 个 wot-ui tools | ||
| wot-ui-v2 Skill AI 知道何时、如何选择和使用组件 | ||
| Instructions AI 在生成代码前主动查询真实组件知识 | ||
| ``` | ||
| 安装完成后可直接使用 `wot`: | ||
| 使用其他客户端时,只需替换 client id: | ||
| ```bash | ||
| wot list | ||
| ``` | ||
| | Claude Code | Cursor | VS Code | Codex | | ||
| | --- | --- | --- | --- | | ||
| | `claude` | `cursor` | `vscode` | `codex` | | ||
| 如果只想临时运行,也可以使用包执行器,不会修改项目依赖: | ||
| 同时使用多个 AI 客户端时,可以一次完成全部项目级配置: | ||
| ```bash | ||
| pnpm dlx @wot-ui/cli@latest list | ||
| npx -y @wot-ui/cli@latest list | ||
| yarn dlx @wot-ui/cli@latest list | ||
| bunx @wot-ui/cli@latest list | ||
| wot agent init --client all | ||
| wot agent doctor --client all --timeout 30000 | ||
| ``` | ||
| `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`。 | ||
| `--client all` 在 project scope 下会处理 Claude Code、Cursor、VS Code 和 Codex;在 user scope 下只处理支持用户级配置的客户端。 | ||
| 如果你在仓库内本地调试,推荐直接运行源码入口,而不是依赖全局命令: | ||
| 配置完成后重启客户端;如果出现“信任项目”或“批准 MCP Server”的提示,请按客户端指引确认。 | ||
| ```bash | ||
| pnpm exec tsx src/index.ts list | ||
| ``` | ||
| ## ✨ 它解决什么问题 | ||
| ## 快速开始 | ||
| 没有上下文的 AI 容易混用 Vue 组件库 API、使用不存在的属性,或者生成与项目版本不匹配的代码。Open Wot 在 AI 写代码前补上准确的 wot-ui 上下文: | ||
| ```bash | ||
| wot list | ||
| wot list button | ||
| wot info Button | ||
| wot demo Button basic | ||
| wot doc Button | ||
| wot token Button | ||
| wot changelog | ||
| wot doctor ./my-project | ||
| wot usage ./my-project | ||
| wot lint ./my-project | ||
| wot agent list | ||
| wot agent init --client cursor | ||
| wot mcp | ||
| ```mermaid | ||
| flowchart LR | ||
| A["wot-ui 文档 · API · Demo · Changelog"] --> B["Open Wot 离线知识库"] | ||
| B --> C["CLI"] | ||
| B --> D["MCP · 8 tools"] | ||
| B --> E["Skills + Instructions"] | ||
| D --> F["Cursor · Claude Code · VS Code · Codex"] | ||
| E --> F | ||
| ``` | ||
| ## 命令说明 | ||
| 它也可以单独作为一个快速的组件知识 CLI: | ||
| ### 组件知识 | ||
| ```console | ||
| $ wot info Button | ||
| - `wot list [keyword]`:列出可用的 wot-ui 组件,支持按名称、中文名、标签、分类和描述过滤 | ||
| - `wot info <component>`:查看组件 props、events、slots、CSS 变量 | ||
| - `wot doc <component>`:输出组件 markdown 文档 | ||
| - `wot demo <component> [name]`:查看 demo 列表或指定 demo 源码 | ||
| - `wot token [component]`:查看组件 CSS 变量与默认值 | ||
| - `wot changelog [version] [component]`:查看版本更新记录 | ||
| Button 按钮 (wd-button) | ||
| 按钮用于触发一个操作,如提交表单或打开链接。 | ||
| ### 项目分析 | ||
| Props: | ||
| - type: string = primary | ||
| - variant: string = base | ||
| - size: string = medium | ||
| - disabled: boolean = false | ||
| - loading: boolean = false | ||
| - `wot doctor [dir]`:检查项目依赖、运行环境与基础集成情况 | ||
| - `wot usage [dir]`:统计 `.vue` 文件中的 `wd-*` 使用情况 | ||
| - `wot lint [dir]`:检查未知组件、空按钮等规则 | ||
| Events: | ||
| - click (`event`): 点击事件 | ||
| ### Agent 接入 | ||
| Slots: | ||
| - default: 按钮内容 | ||
| ``` | ||
| #### 复制给 AI,一键接入 | ||
| ## 给 AI 使用(推荐) | ||
| 将下面这段提示词复制到 Claude Code、Cursor、VS Code 或 Codex,让当前 AI Agent 自动完成接入: | ||
| `agent init` 是推荐入口,它会同时配置 MCP、安装 Skill,并写入由 open-wot 管理的项目 Instructions。 | ||
| ```text | ||
| 请在当前项目根目录接入 wot-ui 的 AI 开发能力,并直接执行所需命令: | ||
| 1. 确认 Node.js 版本不低于 20。 | ||
| 2. 执行 `npm install -g @wot-ui/cli@latest` 安装或更新全局 CLI。不要使用 sudo;如果全局安装因权限受限而失败,改用 `npx -y @wot-ui/cli@latest` 运行后续命令,并在结果中说明。 | ||
| 3. 识别你当前所在的 AI 客户端,并使用对应的 client id:Claude Code 使用 claude,Cursor 使用 cursor,VS Code 使用 vscode,Codex 使用 codex。只配置当前客户端;如果无法确定,请先询问我,不要猜测。 | ||
| 4. 执行 `wot agent init --client <client-id> --scope project --with mcp,skill,instructions --yes` 完成项目级接入;不要手动覆盖现有配置。若上一步回退到 npx,则用 `npx -y @wot-ui/cli@latest` 代替 `wot`。 | ||
| 5. 执行 `wot agent doctor --client <client-id> --scope project --with mcp,skill,instructions` 检查配置、MCP handshake、Skill 和 Instructions;使用与上一步相同的 CLI 执行方式。 | ||
| 6. 最后告诉我:CLI 安装结果、识别到的客户端、修改了哪些文件、doctor 检查结果,以及是否需要我重启客户端或批准项目 MCP。 | ||
| 请保留项目中已有的 MCP Server 和用户内容;如果命令失败,不要绕过安全检查,说明具体原因和建议的处理方式。 | ||
| ```bash | ||
| wot agent init --client cursor | ||
| wot agent status --client cursor | ||
| wot agent doctor --client cursor | ||
| ``` | ||
| 这段提示词会优先安装全局 `wot` 命令,并在权限受限时安全回退到 `npx`。生成的 MCP 配置、安装的 Skill 和 Instructions 会持久保留在项目中;`agent init` 是幂等操作,重复执行不会重复添加配置。 | ||
| 整个生命周期都可以通过 CLI 管理: | ||
| 也可以手动执行: | ||
| ```bash | ||
| wot agent list | ||
| wot agent init --client cursor --dry-run | ||
| wot agent init --client cursor | ||
@@ -124,57 +132,99 @@ wot agent status --client cursor | ||
| `agent init` 默认同时安装三项能力: | ||
| 多客户端项目: | ||
| - 在客户端项目配置中注册 `wot-ui` MCP Server | ||
| - 安装仓库内置的 `wot-ui-v2` Skill | ||
| - 在 `AGENTS.md` 或 `CLAUDE.md` 中维护 open-wot 自己拥有的 Instructions 区块 | ||
| ```bash | ||
| wot agent init --client all | ||
| wot agent status --client all | ||
| wot agent doctor --client all --timeout 30000 | ||
| wot agent remove --client all --dry-run | ||
| ``` | ||
| 当前 npm 发布包只随附以下两个 Skill: | ||
| - `--dry-run` 只展示变更计划,不写文件。 | ||
| - 交互式写操作会请求确认;Agent 或 CI 中显式传入 `--yes`。 | ||
| - 重复执行 `init` 不会重复插入配置。 | ||
| - `remove` 只移除 open-wot 管理的内容,不覆盖其他 Server 或用户配置。 | ||
| - 默认安装面向组件使用者的 `wot-ui-v2` Skill;仓库维护 Skill `wot-ui-cli` 不会默认安装。 | ||
| | Skill | 主要用途 | 随 npm 发布 | `agent init` 默认安装 | | ||
| | --- | --- | --- | --- | | ||
| | `wot-ui-v2` | 组件选型、API 查询、页面生成与组件问题排查 | 是 | 是 | | ||
| | `wot-ui-cli` | CLI、MCP、数据提取与 open-wot 仓库维护 | 是 | 否 | | ||
| 只接入部分能力: | ||
| 可通过 `--with` 限制能力范围: | ||
| ```bash | ||
| wot agent init --client codex --with mcp | ||
| wot agent init --client claude --with skill,instructions | ||
| wot agent status --client claude --with skill,instructions | ||
| wot agent doctor --client claude --with skill,instructions | ||
| wot agent init --client cursor --dry-run | ||
| ``` | ||
| `status` 和 `doctor` 只检查 `--with` 选中的能力;未选择 MCP 时,`doctor` 不会启动 MCP Server。 | ||
| <details> | ||
| <summary><strong>不想自己操作?复制这段话给 AI</strong></summary> | ||
| `init` 和 `remove` 支持 `--dry-run`。写操作在交互式终端中会请求确认;脚本和 CI 必须显式传入 `--yes`。重复执行是幂等的,删除操作只移除 `wot-ui` MCP 条目、未修改的内置 Skill 文件和 open-wot 托管 Instructions 区块。 | ||
| ```text | ||
| 请在当前项目中接入 wot-ui 的 AI 开发能力: | ||
| 若 Instructions 中的 open-wot 标记残缺、顺序错误或重复,CLI 会拒绝修改,避免误删用户内容。 | ||
| 1. 确认 Node.js >= 20。 | ||
| 2. 安装或更新 `@wot-ui/cli@latest`;优先全局安装,不要使用 sudo。权限受限时改用 `npx -y @wot-ui/cli@latest` 执行后续命令。 | ||
| 3. 识别当前客户端:Claude Code=claude、Cursor=cursor、VS Code=vscode、Codex=codex。无法确定时先询问我。 | ||
| 4. 执行 `wot agent init --client <client-id> --scope project --with mcp,skill,instructions --yes`。 | ||
| 5. 执行 `wot agent doctor --client <client-id> --scope project --with mcp,skill,instructions`。 | ||
| 6. 告诉我修改了哪些文件、doctor 结果,以及是否需要重启客户端或批准 MCP。 | ||
| ### 通用参数 | ||
| 请保留已有 MCP Server 和用户内容;如果安全检查失败,说明原因,不要绕过。 | ||
| ``` | ||
| 多数查询命令支持以下参数: | ||
| </details> | ||
| - `--format text`(默认)或 `--format json`:输出格式 | ||
| - `--version <ver>`:指定 wot-ui 版本,支持以下格式: | ||
| - `2.0`(minor,自动解析到最新 patch) | ||
| - `2.0.4`(exact patch) | ||
| - `latest`(始终使用最新稳定版) | ||
| - 不传时自动从项目 `node_modules/@wot-ui/ui` 或 `package.json` 依赖声明检测,检测不到则回退到最新版 | ||
| ## 在终端使用 | ||
| 示例: | ||
| 推荐全局安装: | ||
| ```bash | ||
| wot info Button --version 2.0.0 | ||
| wot doc Button --version 2.0 | ||
| wot list --format json --version latest | ||
| npm install -g @wot-ui/cli@latest | ||
| wot -V | ||
| ``` | ||
| ## MCP 集成 | ||
| 一次性查询也可以直接使用 package runner: | ||
| `wot mcp` 保留默认启动 stdio Server 的行为;在脚本或文档中也可以使用语义更明确的 `wot mcp serve`。 | ||
| ```bash | ||
| npx -y @wot-ui/cli@latest info Button | ||
| pnpm dlx @wot-ui/cli@latest info Button | ||
| ``` | ||
| 推荐使用 CLI 自动配置: | ||
| ### 组件知识 | ||
| | 命令 | 用途 | | ||
| | --- | --- | | ||
| | `wot list [keyword]` | 按名称、中文名、标签、分类或描述查找组件 | | ||
| | `wot info <component>` | 查询 props、events、slots 和 CSS 变量 | | ||
| | `wot doc <component>` | 获取完整 Markdown 文档 | | ||
| | `wot demo <component> [name]` | 查看 demo 列表或指定 demo 源码 | | ||
| | `wot token [component]` | 查询组件 CSS 变量 | | ||
| | `wot changelog [versionOrComponent] [component]` | 按版本或组件查询更新记录 | | ||
| ```bash | ||
| wot list button | ||
| wot info Button | ||
| wot demo Button demo-1 | ||
| wot token Button | ||
| ``` | ||
| ### 项目分析 | ||
| | 命令 | 用途 | | ||
| | --- | --- | | ||
| | `wot doctor [dir]` | 检查依赖、运行环境和基础集成 | | ||
| | `wot usage [dir]` | 统计 `.vue` 文件中的 `wd-*` 使用情况 | | ||
| | `wot lint [dir]` | 检查未知组件、空按钮等问题 | | ||
| ### 版本与结构化输出 | ||
| ```bash | ||
| wot info Button --version 2.0 | ||
| wot info Button --version 2.0.4 | ||
| wot list --version latest --format json | ||
| ``` | ||
| 不传 `--version` 时,CLI 会依次检查项目安装版本、依赖声明和最新离线数据。查询命令支持 `--format text|json|markdown`;结构化结果写入 stdout,诊断信息保持在 stderr。 | ||
| ## 只配置 MCP | ||
| 如果只需要 MCP,不需要 Skill 和 Instructions: | ||
| ```bash | ||
| wot mcp list | ||
@@ -184,7 +234,8 @@ wot mcp init --client cursor | ||
| wot mcp doctor --client cursor | ||
| wot mcp remove --client cursor | ||
| ``` | ||
| 支持的客户端和 project scope 配置位置: | ||
| 支持的项目配置: | ||
| | Client | 配置文件 | 根字段 | | ||
| | Client | 文件 | 配置根字段 | | ||
| | --- | --- | --- | | ||
@@ -196,19 +247,22 @@ | Claude Code | `.mcp.json` | `mcpServers` | | ||
| Claude Code、Cursor 和 Codex 同时支持 `--scope user`;VS Code 当前使用 project scope。管理命令支持 `--format json`,`init`、`status`、`doctor`、`remove` 和 `print` 支持通过 `--pin [version]` 固定生成配置中的 `@wot-ui/cli` 版本;只有会写文件的 `init` 和 `remove` 支持 `--dry-run`。 | ||
| ```bash | ||
| wot mcp print --client cursor # 只预览配置 | ||
| wot mcp init --client cursor --dry-run # 预览文件变更 | ||
| wot mcp init --client all # 配置所有客户端 | ||
| wot mcp init --client codex --pin # 固定当前 CLI 版本 | ||
| ``` | ||
| dry-run 和 JSON 结果只输出托管配置节点的安全预览,不输出配置文件完整内容或其他 Server 的环境变量。写操作还可以使用 `--client all` 一次处理所有支持当前 scope 的客户端。 | ||
| Claude Code、Cursor 和 Codex 支持 `--scope user`;VS Code 当前使用 project scope。`doctor` 会验证配置和真实 MCP handshake,并在客户端支持时继续检查注册状态。 | ||
| Codex adapter 会在写入前后验证完整 TOML。若现有配置使用 `[mcp_servers.wot-ui.env]` 等外部嵌套子表,CLI 会拒绝自动接管或删除;请先将自定义字段迁移到主 Server 定义,再重新执行命令。 | ||
| 直接启动 stdio Server: | ||
| `doctor` 分三层检查配置、MCP handshake 和客户端注册状态。Handshake 会验证 Server 名称以及 `wot_status`、`wot_list` 核心工具;Claude Code 和 Codex 会进一步调用客户端 CLI 查询注册状态。Cursor、VS Code 没有稳定查询接口时会显示 `server-ready`,提示用户重启客户端并在 MCP 面板确认,而不会将它描述为客户端已经就绪。需要用户批准或信任项目时退出码为 `2`,配置或 handshake 失败时退出码为 `1`。 | ||
| 只需要查看配置而不写文件时: | ||
| ```bash | ||
| wot mcp print --client vscode | ||
| wot mcp # 默认启动 | ||
| wot mcp serve # 语义明确的等价写法 | ||
| ``` | ||
| 也可以手动配置。推荐使用无需全局安装的 `npx` 方式: | ||
| <details> | ||
| <summary><strong>手动配置 MCP</strong></summary> | ||
| 将以下配置加入支持 MCP 的客户端: | ||
| 自动配置默认使用 `npx` 启动 Server,避免桌面应用读取不到终端的全局 `PATH`: | ||
@@ -226,3 +280,3 @@ ```json | ||
| 如果已经全局安装 CLI,并且 AI 客户端可以从 `PATH` 中找到 `wot`,也可以使用: | ||
| 已全局安装且客户端能够找到 `wot` 时,也可以使用: | ||
@@ -240,128 +294,167 @@ ```json | ||
| 桌面应用不一定会继承终端中的 npm 全局 `PATH`,因此默认配置和 `wot mcp init` 仍使用兼容性更好的 `npx` 方式。 | ||
| </details> | ||
| 当前 MCP Server 提供以下 tools: | ||
| <details> | ||
| <summary><strong>8 个 MCP tools</strong></summary> | ||
| | Tool | 功能 | 主要参数 | | ||
| | --- | --- | --- | | ||
| | `wot_status` | 查看 MCP Server 与 `@wot-ui/cli` 状态,包括当前版本、是否有 CLI 更新及更新命令。 | 无 | | ||
| | `wot_list` | 列出当前离线知识库中的组件摘要,不包含完整文档、API 与 demo 源码,适合在生成页面前发现可用组件。 | `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` | | ||
| | Tool | 能力 | | ||
| | --- | --- | | ||
| | `wot_status` | Server、CLI 版本与更新状态 | | ||
| | `wot_list` | 组件发现与摘要 | | ||
| | `wot_info` | props、events、slots、CSS 变量 | | ||
| | `wot_doc` | 完整组件文档 | | ||
| | `wot_demo` | demo 摘要或指定示例源码 | | ||
| | `wot_token` | 组件 CSS 变量 | | ||
| | `wot_changelog` | 版本与组件更新记录 | | ||
| | `wot_lint` | 项目中的 wot-ui 使用问题 | | ||
| 其中 `version` 支持与 CLI 一致的写法,例如 `2.0`、`2.0.4`、`latest`;不传时会按项目依赖或离线数据自动解析。 | ||
| </details> | ||
| 为控制 Agent 上下文占用,MCP 的 `wot_list` 只返回 `name`、`nameZh`、`tag`、`category`、`description` 和 `since`;需要组件 API、文档或示例源码时,再调用 `wot_info`、`wot_doc` 或带具体 demo 名称的 `wot_demo`。CLI 的 `list --format json` 与 `demo --format json` 继续保留原有详细结构,避免影响已有脚本。 | ||
| ## 完整命令速查 | ||
| ## 数据来源 | ||
| <details> | ||
| <summary><strong>展开全部命令和参数</strong></summary> | ||
| 当前版本聚焦 `wot-ui v2`。仓库内的离线数据来自 `wot-ui/wot-ui` 源码,主要提取自: | ||
| ### 组件知识与项目分析 | ||
| - `docs/component/*.md` | ||
| - `docs/guide/changelog.md` | ||
| - `src/uni_modules/wot-ui/components/*/index.scss` | ||
| | 命令 | 说明 | | ||
| | --- | --- | | ||
| | `wot list [keyword]` | 查找组件 | | ||
| | `wot info <component>` | 查询组件 API | | ||
| | `wot doc <component>` | 获取完整文档 | | ||
| | `wot demo <component> [name]` | 查询 demo | | ||
| | `wot token [component]` | 查询 CSS 变量 | | ||
| | `wot changelog [versionOrComponent] [component]` | 查询更新记录 | | ||
| | `wot doctor [dir]` | 诊断项目环境 | | ||
| | `wot usage [dir]` | 分析组件使用情况 | | ||
| | `wot lint [dir]` | 检查组件使用问题 | | ||
| `data/` 目录保存每个 stable patch 版本的独立快照(`v2.0.0.json`、`v2.0.1.json`、…),以及一个始终指向最新版的 `v2.json`。 | ||
| 以上查询命令都支持 `--version <version>` 和 `--format text|json|markdown`。 | ||
| ### 更新数据 | ||
| ### Agent | ||
| **全量同步所有历史 tag(推荐,首次或需要补全历史版本时使用):** | ||
| | 命令 | 说明 | | ||
| | --- | --- | | ||
| | `wot agent list` | 列出支持和检测到的客户端 | | ||
| | `wot agent init` | 初始化 MCP、Skill 和 Instructions | | ||
| | `wot agent status` | 检查三类能力的配置状态 | | ||
| | `wot agent doctor` | 检查文件并执行真实 MCP handshake | | ||
| | `wot agent remove` | 删除 open-wot 管理的接入内容 | | ||
| ```bash | ||
| pnpm sync:clone | ||
| ``` | ||
| `init`、`status`、`doctor` 和 `remove` 支持: | ||
| 克隆 wot-ui 仓库(`--filter=tree:0 --no-checkout`,不下载文件树),按所有 stable tag 逐一 checkout + 提取,已有快照自动跳过。 | ||
| - `--client auto|all|claude|cursor|vscode|codex` | ||
| - `--scope project|user` | ||
| - `--with mcp,skill,instructions` | ||
| - `--cwd <directory>` | ||
| - `--format text|json` | ||
| - `--pin [version]` | ||
| **仅更新最新版本(快速,CI 单版本触发时使用):** | ||
| 其中 `init`、`remove` 额外支持 `--dry-run` 和 `--yes`,`doctor` 支持 `--timeout <milliseconds>`。 | ||
| ### MCP | ||
| | 命令 | 说明 | | ||
| | --- | --- | | ||
| | `wot mcp` | 启动 stdio Server | | ||
| | `wot mcp serve` | 显式启动 stdio Server | | ||
| | `wot mcp list` | 列出客户端和检测结果 | | ||
| | `wot mcp init` | 写入 MCP 配置 | | ||
| | `wot mcp status` | 检查 MCP 配置 | | ||
| | `wot mcp doctor` | 验证配置、handshake 和客户端状态 | | ||
| | `wot mcp remove` | 删除托管的 MCP 配置 | | ||
| | `wot mcp print` | 输出单个客户端的配置片段 | | ||
| `init`、`status`、`doctor`、`remove` 和 `print` 支持 `--client`、`--scope`、`--cwd`、`--format` 和 `--pin`;`init`、`remove` 额外支持 `--dry-run`、`--yes`,`doctor` 支持 `--timeout`。`list` 支持 `--cwd` 和 `--format`;`print` 必须指定一个具体客户端,不能使用 `auto` 或 `all`。 | ||
| 随时可以查看 CLI 自带帮助: | ||
| ```bash | ||
| pnpm extract:clone | ||
| wot --help | ||
| wot agent init --help | ||
| wot mcp doctor --help | ||
| ``` | ||
| **使用本地已有的 wot-ui 仓库:** | ||
| </details> | ||
| ```bash | ||
| # 全量多版本 | ||
| pnpm sync --wot-dir ../wot-ui | ||
| ## 安全设计 | ||
| # 单个版本,手动指定 checkout 后提取 | ||
| pnpm extract --wot-dir ../wot-ui --output data/v2.0.4.json | ||
| ``` | ||
| open-wot 会修改客户端配置,因此写入流程默认保守: | ||
| ## 开发本仓库 | ||
| - 先计算 ChangePlan,再确认或执行。 | ||
| - 支持 `--dry-run` 和 JSON 预览。 | ||
| - 保留已有 Server、JSONC 注释和非托管 TOML。 | ||
| - 使用原子写入,并在失败时回滚。 | ||
| - 遇到非法配置或无法安全接管的结构时直接停止。 | ||
| - Agent Instructions 使用明确的托管标记,避免误删用户内容。 | ||
| 当前根目录就是主发布包,核心源码位于 `src`,离线数据位于 `data`,提取脚本位于 `scripts`。 | ||
| ## 开发 open-wot | ||
| ### 环境要求 | ||
| 环境要求:Node.js `>= 20`、pnpm `10.25.x`。 | ||
| - Node.js `>= 20` | ||
| - pnpm `10.x` | ||
| ### 安装与开发 | ||
| ### 安装依赖 | ||
| ```bash | ||
| pnpm install | ||
| pnpm dev # 监听源码并持续构建 dist/ | ||
| ``` | ||
| ### 常用开发命令 | ||
| 也可以直接运行 TypeScript 源码: | ||
| ```bash | ||
| pnpm lint # ESLint 检查 | ||
| pnpm typecheck # TypeScript 类型检查 | ||
| pnpm test # 单元测试 | ||
| pnpm build # 构建产物到 dist/ | ||
| pnpm compress # 压缩 data/*.json → data/*.json.gz(发布前自动执行) | ||
| pnpm exec tsx src/index.ts list | ||
| pnpm exec tsx src/index.ts info Button --version 2.0 | ||
| pnpm exec tsx src/index.ts mcp | ||
| ``` | ||
| ### 本地调试 CLI | ||
| 调试最终构建产物: | ||
| 直接运行源码入口最方便: | ||
| ```bash | ||
| pnpm exec tsx src/index.ts list | ||
| pnpm exec tsx src/index.ts info Button | ||
| pnpm exec tsx src/index.ts info Button --version 2.0.0 | ||
| pnpm exec tsx src/index.ts doc Button --version 2.0 | ||
| pnpm build | ||
| node dist/index.mjs list | ||
| node dist/index.mjs mcp doctor --client cursor | ||
| ``` | ||
| 如果要调试构建产物: | ||
| ### 提交前验证 | ||
| ```bash | ||
| pnpm lint | ||
| pnpm typecheck | ||
| pnpm test | ||
| pnpm build | ||
| node dist/index.mjs list | ||
| node dist/index.mjs info Button --version 2.0.0 | ||
| ``` | ||
| ### 本地调试 MCP | ||
| 测试开发: | ||
| ```bash | ||
| pnpm exec tsx src/index.ts mcp | ||
| pnpm test:watch | ||
| pnpm test:coverage | ||
| ``` | ||
| MCP 走 stdio,终端无交互输出属于正常现象。若要查看 tools 与 prompts 的调用过程,建议配合 MCP Inspector 或编辑器内置 MCP 客户端调试。 | ||
| CI 会在 Node.js 20/22 以及 Ubuntu、Windows、macOS 上执行对应检查。 | ||
| ## 自动化流程 | ||
| ### 更新离线数据 | ||
| - `.github/workflows/ci.yml`:在 `push`/`PR` 时执行 lint、typecheck、build、test(多 OS × Node 版本矩阵) | ||
| - `.github/workflows/sync.yml`:每日 02:00 UTC 自动检测 `@wot-ui/ui` 最新版本,有更新时拉取全量多版本快照并创建同步 PR;也可手动触发单版本提取 | ||
| - `.github/workflows/release.yml`:`v*` tag 触发自动发布 `@wot-ui/cli` 到 npm;`prepublishOnly` 依次执行 `pnpm build` 和 `pnpm compress`,发布包携带压缩后的组件数据及 `versions.json` 版本索引 | ||
| - `.github/workflows/coverage-upload.yml`:`v*` tag 触发,上传测试覆盖率到 Codecov | ||
| ```bash | ||
| pnpm sync:clone # 同步全部 stable 快照 | ||
| pnpm extract:clone # 只提取最新版本 | ||
| ``` | ||
| ## 当前边界 | ||
| 已有本地 wot-ui 仓库时: | ||
| - 当前仅支持 `wot-ui v2` | ||
| - `usage` 与 `lint` 当前聚焦 `.vue` 文件中的 `<wd-*>` 标签及相关 import | ||
| - 提取脚本优先从 SCSS 源码解析 CSS 变量,并在必要时回退到 markdown 表格 | ||
| ```bash | ||
| pnpm sync --wot-dir ../wot-ui | ||
| pnpm extract --wot-dir ../wot-ui --output data/v2.0.4.json | ||
| ``` | ||
| ## 相关文档 | ||
| 修改 CLI、MCP、数据提取、Skill 或发布文件时,需要执行的定向验证不同。完整仓库结构、验证矩阵、打包与提交流程见 [CONTRIBUTING.md](./CONTRIBUTING.md)。 | ||
| - [CONTRIBUTING.md](CONTRIBUTING.md):贡献与开发流程 | ||
| ## 当前边界 | ||
| - 当前仅支持 wot-ui v2。 | ||
| - `usage` 与 `lint` 聚焦 `.vue` 文件中的 `<wd-*>` 标签及相关 import。 | ||
| - 提取脚本优先从 SCSS 解析 CSS 变量,必要时回退到 Markdown 表格。 | ||
| ## License | ||
| [MIT](./LICENSE) License © wot-ui | ||
| [MIT](./LICENSE.md) License © wot-ui |
| # Wot UI CLI Overview | ||
| 本文件根据本仓库 README 整理,目标是让 Agent 快速理解 `@wot-ui/cli` 的能力边界、命令分组、MCP 接入方式、开发调试路径与数据来源。 | ||
| ## 目录 | ||
| ## Package Identity | ||
| - [定位](#定位) | ||
| - [命令矩阵](#命令矩阵) | ||
| - [Agent 与 MCP](#agent-与-mcp) | ||
| - [仓库目录](#仓库目录) | ||
| - [开发与验证](#开发与验证) | ||
| - [数据更新](#数据更新) | ||
| - [发布包与发布](#发布包与发布) | ||
| - 包名:`@wot-ui/cli` | ||
| - 仓库:open-wot | ||
| ## 定位 | ||
| - npm 包:`@wot-ui/cli` | ||
| - 可执行命令:`wot` | ||
| - 核心定位:wot-ui 的 AI 工具链仓库,提供 CLI、MCP Server、离线组件知识库与数据提取脚本。 | ||
| - Node.js:`>= 20` | ||
| - 包管理器:pnpm `10.25.x` | ||
| - 数据范围:wot-ui v2 | ||
| - 核心能力:CLI 查询、项目分析、MCP Server、Agent 接入、多版本离线知识和数据提取 | ||
| ## Repository Positioning | ||
| ## 命令矩阵 | ||
| - 面向 wot-ui v2 的组件知识查询工具。 | ||
| - 面向本地项目的组件使用分析与 lint 工具。 | ||
| - 面向 AI 客户端的 MCP stdio 服务。 | ||
| - 面向仓库维护者的数据提取与同步工作流。 | ||
| ### 组件知识与项目分析 | ||
| ## Core Capabilities | ||
| | 命令 | 用途 | | ||
| | --- | --- | | ||
| | `wot list [keyword]` | 按名称、中文名、标签、分类或描述查找组件 | | ||
| | `wot info <component>` | props、events、slots、CSS 变量 | | ||
| | `wot doc <component>` | 完整 Markdown 文档 | | ||
| | `wot demo <component> [name]` | demo 列表或指定源码 | | ||
| | `wot token [component]` | 组件 CSS 变量 | | ||
| | `wot changelog [versionOrComponent] [component]` | 版本或组件更新记录 | | ||
| | `wot doctor [dir]` | 项目环境与依赖诊断 | | ||
| | `wot usage [dir]` | `.vue` 文件中的组件使用统计 | | ||
| | `wot lint [dir]` | 未知组件、空按钮等规则 | | ||
| ### Component Knowledge | ||
| 查询命令支持 `--version <version>` 和 `--format text|json|markdown`。 | ||
| - `list`:列出可用组件。 | ||
| - `info <Component>`:查看 props、events、slots、CSS 变量。 | ||
| - `doc <Component>`:输出组件 markdown 文档。 | ||
| - `demo <Component> [name]`:查看 demo 列表或指定 demo 源码。 | ||
| - `token [Component]`:查看组件 CSS 变量与默认值。 | ||
| - `changelog [version] [component]`:查看版本更新记录。 | ||
| ### Agent | ||
| ### Project Analysis | ||
| | 子命令 | 用途 | | ||
| | --- | --- | | ||
| | `list` | 列出支持和检测到的客户端 | | ||
| | `init` | 初始化 MCP、Skill 和 Instructions | | ||
| | `status` | 检查三类能力 | | ||
| | `doctor` | 检查文件和真实 MCP handshake | | ||
| | `remove` | 删除 open-wot 托管内容 | | ||
| - `doctor [dir]`:检查项目依赖、运行环境与基础集成情况。 | ||
| - `usage [dir]`:统计 `.vue` 文件中的 `wd-*` 使用情况。 | ||
| - `lint [dir]`:检查未知组件、空按钮等规则。 | ||
| `init/status/doctor/remove` 支持 `--client auto|all|claude|cursor|vscode|codex`、`--scope`、`--with`、`--cwd`、`--format` 和 `--pin`。`init/remove` 支持 `--dry-run`、`--yes`;`doctor` 支持 `--timeout`。 | ||
| ### MCP Server | ||
| ### MCP | ||
| - `mcp`:启动 MCP stdio server。 | ||
| | 子命令 | 用途 | | ||
| | --- | --- | | ||
| | `wot mcp` / `wot mcp serve` | 启动 stdio Server | | ||
| | `list` | 客户端检测 | | ||
| | `init` | 写入 MCP 配置 | | ||
| | `status` | 检查配置 | | ||
| | `doctor` | 配置、handshake 和客户端状态检查 | | ||
| | `remove` | 删除托管配置 | | ||
| | `print` | 输出单客户端配置片段 | | ||
| ## Typical User Flows | ||
| ## Agent 与 MCP | ||
| ### Query Component Knowledge Through CLI | ||
| 支持的 project scope 配置: | ||
| 常用顺序: | ||
| | Client | 文件 | | ||
| | --- | --- | | ||
| | Claude Code | `.mcp.json` | | ||
| | Cursor | `.cursor/mcp.json` | | ||
| | VS Code | `.vscode/mcp.json` | | ||
| | Codex | `.codex/config.toml` | | ||
| 1. `wot list` | ||
| 2. `wot info Button` | ||
| 3. `wot demo Button basic` | ||
| 4. `wot doc Button` | ||
| 5. `wot token Button` | ||
| 一次接入所有客户端: | ||
| ### Analyze A Local Project | ||
| 常用顺序: | ||
| 1. `wot doctor ./my-project` | ||
| 2. `wot usage ./my-project` | ||
| 3. `wot lint ./my-project` | ||
| ### Run MCP In A Client | ||
| 推荐自动接入: | ||
| ```bash | ||
| wot agent init --client cursor | ||
| wot agent status --client cursor | ||
| wot agent doctor --client cursor | ||
| wot agent init --client all | ||
| wot agent doctor --client all --timeout 30000 | ||
| ``` | ||
| 只管理 MCP 时使用: | ||
| 默认 Agent 接入安装 MCP、`wot-ui-v2` Skill 和托管 Instructions。`wot-ui-cli` Skill 随 npm 包发布,但不默认安装到组件使用项目。 | ||
| ```bash | ||
| wot mcp init --client cursor | ||
| wot mcp print --client cursor | ||
| wot mcp status --client cursor | ||
| wot mcp doctor --client cursor | ||
| wot mcp remove --client cursor | ||
| ``` | ||
| MCP 提供 8 个 tools: | ||
| 所有写操作支持 `--dry-run`;Agent 或 CI 非交互执行时显式传入 `--yes`。 | ||
| 典型配置: | ||
| ```json | ||
| { | ||
| "mcpServers": { | ||
| "wot-ui": { | ||
| "command": "npx", | ||
| "args": ["-y", "@wot-ui/cli", "mcp"] | ||
| } | ||
| } | ||
| } | ||
| ```text | ||
| wot_status | ||
| wot_list | ||
| wot_info | ||
| wot_doc | ||
| wot_demo | ||
| wot_token | ||
| wot_changelog | ||
| wot_lint | ||
| ``` | ||
| 当前 README 明确列出的 MCP tools 有: | ||
| ## 仓库目录 | ||
| - `wot_list` | ||
| - `wot_info` | ||
| - `wot_doc` | ||
| - `wot_demo` | ||
| - `wot_token` | ||
| - `wot_changelog` | ||
| - `wot_lint` | ||
| ## Common Flags | ||
| 多数查询命令支持: | ||
| - `--format text` | ||
| - `--format json` | ||
| - `--version v2` | ||
| ## Install And Run | ||
| ### Global Install | ||
| ```bash | ||
| npm install -g @wot-ui/cli | ||
| ```text | ||
| src/commands CLI 子命令 | ||
| src/application MCP/Agent 编排和 ChangePlan | ||
| src/mcp Server、tools、prompts、adapters | ||
| src/data loader 和版本解析 | ||
| src/utils 文件、终端、项目扫描等公共能力 | ||
| scripts extract、sync、compress | ||
| data 多版本离线数据 | ||
| skills Agent Skills | ||
| test 测试 | ||
| ``` | ||
| 安装后直接用 `wot`。 | ||
| ## 开发与验证 | ||
| ### Source Mode In This Repo | ||
| 安装和调试: | ||
| ```bash | ||
| pnpm exec tsx src/index.ts list | ||
| pnpm install | ||
| pnpm dev | ||
| pnpm exec tsx src/index.ts info Button | ||
| pnpm exec tsx src/index.ts mcp | ||
| pnpm build | ||
| node dist/index.mjs info Button | ||
| ``` | ||
| 适合本地调试源码,不依赖全局安装。 | ||
| 提交前: | ||
| ### Built Artifact Mode | ||
| ```bash | ||
| pnpm lint | ||
| pnpm typecheck | ||
| pnpm test | ||
| pnpm build | ||
| node dist/index.mjs list | ||
| ``` | ||
| 适合验证构建产物行为。 | ||
| 定向要求: | ||
| ## MCP Operational Notes | ||
| - CLI 输出变化:运行真实命令,同步 README 和命令测试。 | ||
| - MCP/Agent:使用隔离目录验证 dry-run、幂等、doctor 和 remove。 | ||
| - 数据结构:验证历史快照兼容 commands 与 MCP。 | ||
| - Skills:验证 `agent init --dry-run` 和 npm 包文件列表。 | ||
| - package exports/files:运行 publint 和 npm pack。 | ||
| - `wot mcp` 走 stdio。 | ||
| - 终端里没有交互输出通常是正常现象。 | ||
| - 若要调试 tool 与 prompt 调用过程,建议配合 MCP Inspector 或编辑器内置 MCP 客户端。 | ||
| ## 数据更新 | ||
| ## Data Source And Extraction | ||
| 离线数据来自上游 wot-ui 的组件 Markdown、changelog 和 SCSS。 | ||
| 当前版本聚焦 `wot-ui v2`。 | ||
| 离线数据主要提取自上游 `wot-ui/wot-ui` 的: | ||
| - `docs/component/*.md` | ||
| - `docs/guide/changelog.md` | ||
| - `src/uni_modules/wot-ui/components/*/index.scss` | ||
| 重新生成数据有两种方式: | ||
| ### Use A Local Wot UI Repo | ||
| ```bash | ||
| pnpm extract:cli --wot-dir ../wot-ui --output data/v2.json | ||
| ``` | ||
| ### Clone Latest Upstream And Extract | ||
| ```bash | ||
| pnpm sync:clone | ||
| pnpm extract:clone | ||
| pnpm sync --wot-dir ../wot-ui | ||
| pnpm extract --wot-dir ../wot-ui --output data/v2.0.4.json | ||
| ``` | ||
| ## Repo Layout | ||
| 提取后运行 `pnpm test && pnpm build`,并检查 `data/versions.json`、目标快照和 `data/v2.json`。 | ||
| - `src`:CLI、MCP 与项目分析源码。 | ||
| - `data`:离线组件元数据。 | ||
| - `scripts`:提取脚本。 | ||
| - `skills`:面向 Agent 的技能说明。 | ||
| - `test`:根包测试。 | ||
| ## 发布包与发布 | ||
| ## Local Development Commands | ||
| ### Environment | ||
| - Node.js `>= 20` | ||
| - pnpm `10.x` | ||
| ### Install | ||
| ```bash | ||
| pnpm install | ||
| ``` | ||
| ### Common Validation | ||
| ```bash | ||
| pnpm lint | ||
| pnpm test:all | ||
| pnpm build:all | ||
| pnpm typecheck:all | ||
| ``` | ||
| ### Package-Level Commands | ||
| ```bash | ||
| pnpm build | ||
| pnpm test | ||
| pnpm typecheck | ||
| pnpm compress | ||
| pnpm exec publint | ||
| npm pack --dry-run --json | ||
| ``` | ||
| ## Agent Guidance | ||
| 发布包应包含 `dist/`、压缩数据、`data/versions.json`、`skills/wot-ui-v2` 和 `skills/wot-ui-cli`。 | ||
| - 如果用户问的是命令怎么用,按“命令组 + 示例命令 + 输出用途”来回答。 | ||
| - 如果用户问的是仓库维护或调试,优先给本仓库中的真实命令和目录。 | ||
| - 如果用户问的是组件本身怎么写页面,不要停留在 CLI 层,应切换到 `wot-ui-v2` skill。 | ||
| - 不要把 `wot` 命令和 `@wot-ui/ui` 组件库 API 混为一谈。 | ||
| 维护者使用 `pnpm release`。bumpp 默认更新版本、提交、创建 tag 并推送;`v*` tag 触发 GitHub Actions 发布。发布后检查 npm 版本、文件和 dist-tags,预发布版本不要占用 `latest`。 |
| --- | ||
| name: wot-ui-cli | ||
| description: '回答、使用、调试 @wot-ui/cli 时使用。关键词:wot、@wot-ui/cli、CLI、MCP、doctor、usage、lint、list、info、doc、demo、token、changelog、extract、wot mcp。适用于命令查询、参数说明、MCP 接入、本地调试、数据提取与 open-wot 仓库维护。' | ||
| argument-hint: '命令名、参数、MCP 场景、调试问题或数据提取需求' | ||
| description: 使用、调试或维护 @wot-ui/cli 与 open-wot 仓库。适用于 wot CLI 命令、Agent/MCP 接入、doctor/usage/lint、组件知识查询、客户端 adapter、离线数据提取、构建测试、发布包检查和仓库开发;如果任务是直接编写 wd-* 组件页面或解释组件 API,应改用 wot-ui-v2 skill。 | ||
| --- | ||
| # Wot UI CLI Skill | ||
| # Wot UI CLI | ||
| 这个 skill 用于让 Agent 在处理 `@wot-ui/cli` 本身相关的问题时,优先基于本仓库 README 与实际命令能力回答,而不是把它误当成纯组件库文档。 | ||
| 处理 `@wot-ui/cli`、MCP、Agent 接入和 open-wot 仓库维护任务时,基于当前源码、`package.json` 和 README 行事,不根据旧文档猜测命令。 | ||
| ## 适用场景 | ||
| ## 路由 | ||
| - 用户询问 `wot` 命令怎么用。 | ||
| - 用户需要区分 `list`、`info`、`doc`、`demo`、`token`、`changelog`、`doctor`、`usage`、`lint`、`mcp`、`extract` 的用途。 | ||
| - 用户要接入 MCP Server,或需要 `wot mcp` 的配置与调试方法。 | ||
| - 用户要在本仓库中调试 `@wot-ui/cli`、验证构建产物、重新提取数据。 | ||
| - 用户的问题本质上是 open-wot 仓库维护问题,而不是单纯的 wot-ui 组件使用问题。 | ||
| - CLI、MCP、Agent、数据提取或仓库开发:使用本 Skill。 | ||
| - 组件选型、页面代码、props、主题和组件问题:使用 `wot-ui-v2` Skill。 | ||
| - 通过 CLI 查询组件知识时,保留 CLI 使用语境;需要生成组件代码时再切换到 `wot-ui-v2`。 | ||
| ## 适用范围 | ||
| ## 工作流 | ||
| - 关注对象是 `@wot-ui/cli` 这个工具包,以及仓库 `open-wot` 的开发维护流程。 | ||
| - 重点覆盖命令能力、通用参数、MCP、离线数据来源、提取流程、本地调试和发布包边界。 | ||
| - 如果任务是生成 `wd-*` 页面代码、解释组件 props 或给出主题定制方案,应优先使用 `wot-ui-v2` skill。 | ||
| 1. 先查看 `README.md`、`package.json#scripts` 和相关源码。 | ||
| 2. 修改 CLI、MCP、prompt、数据结构或 Skill 后,同步测试与 README。 | ||
| 3. 写配置前先运行 `--dry-run`,使用隔离 `--cwd`,不要修改真实用户配置。 | ||
| 4. 修改命令输出后,至少运行一个真实源码或构建产物命令。 | ||
| 5. 提交前运行 `pnpm lint && pnpm typecheck && pnpm test && pnpm build`。 | ||
| ## 推荐流程 | ||
| ## 命令分组 | ||
| 1. 先确认用户是在问 CLI 工具本身,还是在借 CLI 查询组件知识。 | ||
| 2. 如果是命令使用问题,优先按命令类别回答:组件知识、项目分析、MCP、数据提取、仓库开发。 | ||
| 3. 如果是仓库维护问题,优先给出本仓库里的实际调试命令,而不是泛泛而谈。 | ||
| 4. 如果涉及组件内容本身,可引导或切换到 `wot-ui-v2` skill。 | ||
| 组件知识: | ||
| ## 命令分组 | ||
| ```bash | ||
| wot list [keyword] | ||
| wot info <component> | ||
| wot doc <component> | ||
| wot demo <component> [name] | ||
| wot token [component] | ||
| wot changelog [versionOrComponent] [component] | ||
| ``` | ||
| ### 组件知识查询 | ||
| 项目分析: | ||
| - `wot list` | ||
| - `wot info <Component>` | ||
| - `wot doc <Component>` | ||
| - `wot demo <Component> [name]` | ||
| - `wot token [Component]` | ||
| - `wot changelog [version] [component]` | ||
| ```bash | ||
| wot doctor [dir] | ||
| wot usage [dir] | ||
| wot lint [dir] | ||
| ``` | ||
| ### 项目分析 | ||
| Agent 接入: | ||
| - `wot doctor [dir]` | ||
| - `wot usage [dir]` | ||
| - `wot lint [dir]` | ||
| ```bash | ||
| wot agent list | ||
| wot agent init --client cursor | ||
| wot agent status --client cursor | ||
| wot agent doctor --client cursor | ||
| wot agent remove --client cursor | ||
| wot agent init --client all | ||
| ``` | ||
| ### MCP | ||
| MCP 管理: | ||
| - `wot mcp` | ||
| ```bash | ||
| wot mcp | ||
| wot mcp serve | ||
| wot mcp list | ||
| wot mcp init --client cursor | ||
| wot mcp status --client cursor | ||
| wot mcp doctor --client cursor | ||
| wot mcp remove --client cursor | ||
| wot mcp print --client cursor | ||
| ``` | ||
| ### 数据提取与仓库维护 | ||
| 查询命令支持 `--version` 和 `--format text|json|markdown`。Agent/MCP 管理命令支持的具体选项以 `--help` 和源码为准;写操作优先使用 `--dry-run`。 | ||
| - `pnpm extract:cli --wot-dir ../wot-ui --output data/v2.json` | ||
| - `pnpm extract:clone` | ||
| - `pnpm exec tsx src/index.ts <command>` | ||
| - `pnpm build` | ||
| - `node dist/index.mjs <command>` | ||
| ## 仓库开发 | ||
| ## 工作规则 | ||
| ```bash | ||
| pnpm install | ||
| pnpm dev | ||
| pnpm exec tsx src/index.ts list | ||
| pnpm build | ||
| node dist/index.mjs list | ||
| ``` | ||
| - 包名是 `@wot-ui/cli`,实际可执行命令是 `wot`。 | ||
| - 回答命令问题时,优先用仓库 README 中已承诺的行为和参数,不臆造未声明子命令。 | ||
| - 回答本地调试问题时,优先给源码入口:`pnpm exec tsx src/index.ts ...`。 | ||
| - 回答构建产物问题时,再给 `node dist/index.mjs ...`。 | ||
| - 回答 MCP 问题时,要说明 `wot mcp` 走 stdio,终端无交互输出通常是正常现象。 | ||
| - 回答提取逻辑问题时,要说明数据主要来自上游 `wot-ui/wot-ui` 的 markdown 与 SCSS 源码。 | ||
| - 当用户问的是组件知识但入口是 CLI,也要保留“这是通过 CLI 查询组件知识”这一层语义。 | ||
| 更新离线数据: | ||
| ## 参考资料 | ||
| ```bash | ||
| pnpm sync:clone | ||
| pnpm extract:clone | ||
| pnpm sync --wot-dir ../wot-ui | ||
| pnpm extract --wot-dir ../wot-ui --output data/v2.0.4.json | ||
| ``` | ||
| - [Wot UI CLI 概览](./references/overview.md) | ||
| 发布包检查: | ||
| ```bash | ||
| pnpm build | ||
| pnpm compress | ||
| pnpm exec publint | ||
| npm pack --dry-run --json | ||
| ``` | ||
| ## MCP 注意事项 | ||
| - `wot mcp` 使用 stdio,终端无普通输出通常正常。 | ||
| - `doctor` 验证配置和真实 handshake;部分客户端还会检查注册状态。 | ||
| - `--client all` 在 project scope 处理四个支持客户端。 | ||
| - 保留已有 Server、JSONC 注释、非托管 TOML 和用户 Instructions。 | ||
| ## 参考 | ||
| 需要完整命令矩阵、目录职责、验证要求、数据与发布流程时,读取 [references/overview.md](./references/overview.md)。 |
| import { _ as version, c as toComponentSummary, g as name, h as loadMetadataFile, i as lintProject, l as toDemoSummary, m as resolveVersion, n as getCliUpdateStatus, o as findComponent, s as listComponents } from "./update-check-YZ9xD4u9.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 compactJsonText(value) { | ||
| return JSON.stringify(value); | ||
| } | ||
| 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: compactJsonText({ components: listComponents(version$1).map(toComponentSummary) }) | ||
| }] }; | ||
| }); | ||
| 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 ?? []).map(toDemoSummary) }) | ||
| }] }; | ||
| 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 { homedir } from "node:os"; | ||
| import { gunzipSync } from "node:zlib"; | ||
| import { parse } from "@vue/compiler-sfc"; | ||
| //#region package.json | ||
| var name = "@wot-ui/cli"; | ||
| var version = "1.0.5-beta.1"; | ||
| //#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 filterComponents(components, keyword) { | ||
| const normalized = keyword?.trim().toLowerCase(); | ||
| if (!normalized) return components; | ||
| return components.filter((component) => { | ||
| return [ | ||
| component.name, | ||
| component.nameZh, | ||
| component.tag, | ||
| component.category, | ||
| component.description, | ||
| component.descriptionZh | ||
| ].some((value) => value.toLowerCase().includes(normalized)); | ||
| }); | ||
| } | ||
| function toComponentSummary(component) { | ||
| return { | ||
| name: component.name, | ||
| nameZh: component.nameZh, | ||
| tag: component.tag, | ||
| category: component.category, | ||
| description: component.descriptionZh || component.description, | ||
| since: component.since | ||
| }; | ||
| } | ||
| function toDemoSummary(demo) { | ||
| return { | ||
| name: demo.name, | ||
| title: demo.title, | ||
| description: demo.description | ||
| }; | ||
| } | ||
| 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 { version as _, filterComponents as a, toComponentSummary as c, formatStatusLabel as d, writeStderrLine as f, name as g, loadMetadataFile as h, lintProject as i, toDemoSummary as l, resolveVersion as m, getCliUpdateStatus as n, findComponent as o, detectVersion as p, analyzeUsage as r, listComponents as s, checkForCliUpdate as t, formatLogMessage as u }; |
Sorry, the diff of this file is too big to display
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.
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
0
-100%456
25.62%2
-33.33%2412734
-0.03%