@agile-team/wl-skills-ui 1.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (150) hide show
  1. package/CHANGELOG.md +207 -0
  2. package/README.md +623 -0
  3. package/SKILL.md +632 -0
  4. package/bin/wl-ui.js +794 -0
  5. package/design/spec/color.md +41 -0
  6. package/design/spec/spacing.md +28 -0
  7. package/design/spec/typography.md +29 -0
  8. package/design/tokens/base.css +144 -0
  9. package/design/tokens/index.css +8 -0
  10. package/dist/ag-grid-override.scss +1 -0
  11. package/dist/element.scss +1 -0
  12. package/dist/index.scss +1 -0
  13. package/dist/portal.scss +1 -0
  14. package/dist/style-override.scss +1 -0
  15. package/dist/tokens.css +144 -0
  16. package/es/chunk-GBB7LDKQ.js +261 -0
  17. package/es/chunk-MDI2SCUP.js +281 -0
  18. package/es/chunk-QDTNMM7W.js +286 -0
  19. package/es/chunk-YLNAFAWE.js +233 -0
  20. package/es/common-preset.d.ts +38 -0
  21. package/es/common-preset.js +157 -0
  22. package/es/index.d.ts +32 -0
  23. package/es/index.js +1 -0
  24. package/es/presets/security.d.ts +16 -0
  25. package/es/presets/security.js +99 -0
  26. package/es/renderers-BFRSp2BH.d.ts +51 -0
  27. package/es/renderers-DMZbi_Gs.d.ts +51 -0
  28. package/es/renderers-DWhUlI2y.d.ts +47 -0
  29. package/es/types-04qLKHMt.d.ts +62 -0
  30. package/es/types-BrCPjAH6.d.ts +62 -0
  31. package/es/types-QZ1gy9KX.d.ts +63 -0
  32. package/examples/migration-operations-to-renderOps.md +107 -0
  33. package/examples/wk-exempt.example.json +30 -0
  34. package/mcp/server.js +394 -0
  35. package/package.json +130 -0
  36. package/reference/SelectPopupCom.vue +634 -0
  37. package/reference/ag-cell-renders.ts +644 -0
  38. package/reference/define-columns.ts +185 -0
  39. package/reference/jhlc.d.ts +1 -0
  40. package/scanner/coverage.mjs +287 -0
  41. package/scanner/exempt.mjs +127 -0
  42. package/scanner/fix.mjs +216 -0
  43. package/scanner/index.mjs +403 -0
  44. package/scanner/integration.mjs +155 -0
  45. package/scanner/report.mjs +459 -0
  46. package/scanner/rules/_shared.mjs +134 -0
  47. package/scanner/rules/button.mjs +97 -0
  48. package/scanner/rules/color.mjs +118 -0
  49. package/scanner/rules/componentFamily.mjs +184 -0
  50. package/scanner/rules/dialog.mjs +42 -0
  51. package/scanner/rules/form.mjs +86 -0
  52. package/scanner/rules/index.mjs +58 -0
  53. package/scanner/rules/table.mjs +170 -0
  54. package/scanner/rules/tag.mjs +245 -0
  55. package/scanner/snapshot.mjs +198 -0
  56. package/skills/_flows/full-audit.md +65 -0
  57. package/skills/_flows/legacy-skin-align.md +71 -0
  58. package/skills/_flows/new-project-init.md +80 -0
  59. package/skills/_flows/progressive-migrate.md +49 -0
  60. package/skills/_meta/_compat/README.md +47 -0
  61. package/skills/_meta/_compat/editors.json +76 -0
  62. package/skills/_meta/_compat/headers/agents.txt +7 -0
  63. package/skills/_meta/_compat/headers/claude-code.txt +7 -0
  64. package/skills/_meta/_compat/headers/cline.txt +7 -0
  65. package/skills/_meta/_compat/headers/cursor-mdc.txt +5 -0
  66. package/skills/_meta/_compat/headers/github-copilot.txt +5 -0
  67. package/skills/_meta/_compat/headers/kiro.txt +4 -0
  68. package/skills/_meta/_compat/headers/qoder.txt +4 -0
  69. package/skills/_meta/_compat/headers/trae.txt +3 -0
  70. package/skills/_meta/_compat/headers/windsurf.txt +5 -0
  71. package/skills/_meta/_detection.md +81 -0
  72. package/skills/_meta/_registry.md +96 -0
  73. package/skills/element/component-family/SKILL.md +71 -0
  74. package/skills/element/el-dialog/SKILL.md +124 -0
  75. package/skills/element/el-form/SKILL.md +84 -0
  76. package/skills/element/el-table/SKILL.md +86 -0
  77. package/skills/element/el-tag/SKILL.md +127 -0
  78. package/skills/layouts/detail-page/SKILL.md +99 -0
  79. package/skills/layouts/form-dialog/SKILL.md +128 -0
  80. package/skills/layouts/list-page/SKILL.md +111 -0
  81. package/skills/layouts/tree-list/SKILL.md +108 -0
  82. package/skills/ops/audit/SKILL.md +109 -0
  83. package/skills/ops/fix/SKILL.md +52 -0
  84. package/skills/ops/fix/USAGE.md +24 -0
  85. package/skills/ops/migrate/SKILL.md +157 -0
  86. package/skills/ops/scan/SKILL.md +71 -0
  87. package/skills/ops/scan/USAGE.md +16 -0
  88. package/skills/runtime/design-tokens/SKILL.md +88 -0
  89. package/skills/runtime/design-tokens/USAGE.md +19 -0
  90. package/skills/runtime/migration/SKILL.md +106 -0
  91. package/skills/runtime/migration/USAGE.md +34 -0
  92. package/skills/runtime/style-align/SKILL.md +124 -0
  93. package/skills/runtime/style-align/USAGE.md +52 -0
  94. package/skills/vendors/ag-grid/SKILL.md +94 -0
  95. package/skills/vendors/base-table/SKILL.md +57 -0
  96. package/skills/vendors/c-components/SKILL.md +36 -0
  97. package/skills/vendors/custom-wrappers/SKILL.md +45 -0
  98. package/skills/vendors/jh-components/SKILL.md +42 -0
  99. package/skills/vendors/unknown-wrapper/SKILL.md +44 -0
  100. package/standards/engineering/01-import-order.md +46 -0
  101. package/standards/engineering/02-naming.md +54 -0
  102. package/standards/engineering/03-scss-structure.md +59 -0
  103. package/standards/ui/01-table.md +111 -0
  104. package/standards/ui/02-button.md +83 -0
  105. package/standards/ui/03-form.md +118 -0
  106. package/standards/ui/04-tag-status.md +110 -0
  107. package/standards/ui/05-dialog-pagination.md +118 -0
  108. package/styles/element/_card.scss +26 -0
  109. package/styles/element/_descriptions.scss +22 -0
  110. package/styles/element/_dialog.scss +43 -0
  111. package/styles/element/_drawer.scss +22 -0
  112. package/styles/element/_feedback.scss +52 -0
  113. package/styles/element/_form.scss +22 -0
  114. package/styles/element/_navigation.scss +36 -0
  115. package/styles/element/_overlay.scss +32 -0
  116. package/styles/element/_pagination.scss +16 -0
  117. package/styles/element/_steps.scss +32 -0
  118. package/styles/element/_table.scss +13 -0
  119. package/styles/element/_tabs.scss +40 -0
  120. package/styles/element/_tree.scss +27 -0
  121. package/styles/element/_upload.scss +34 -0
  122. package/styles/element/index.scss +17 -0
  123. package/styles/index.scss +21 -0
  124. package/styles/layouts/_detail-page.scss +27 -0
  125. package/styles/layouts/_form-dialog.scss +19 -0
  126. package/styles/layouts/_list-page.scss +47 -0
  127. package/styles/layouts/_tree-list.scss +33 -0
  128. package/styles/layouts/index.scss +6 -0
  129. package/styles/presets/element-only.scss +6 -0
  130. package/styles/presets/full.scss +12 -0
  131. package/styles/presets/security.scss +37 -0
  132. package/styles/presets/skin.scss +14 -0
  133. package/styles/presets/tokens-only.scss +7 -0
  134. package/styles/tokens/index.scss +79 -0
  135. package/styles/vendors/_ag-grid.scss +273 -0
  136. package/styles/vendors/_base-components.scss +480 -0
  137. package/styles/vendors/_base-query-toolbar.scss +253 -0
  138. package/styles/vendors/_base-table.scss +41 -0
  139. package/styles/vendors/_c-components.scss +34 -0
  140. package/styles/vendors/_custom-wrappers.scss +30 -0
  141. package/styles/vendors/_jh-drag-col.scss +63 -0
  142. package/styles/vendors/_jh-pagination.scss +81 -0
  143. package/styles/vendors/_jh-tree.scss +196 -0
  144. package/styles/vendors/_portal.scss +114 -0
  145. package/styles/vendors/index.scss +17 -0
  146. package/templates/ag-grid-page/TPL-AG-GRID.md +76 -0
  147. package/templates/form-dialog/TPL-FORM-DIALOG.md +165 -0
  148. package/templates/list-page/TPL-LIST.md +139 -0
  149. package/templates/tree-list/TPL-TREE-LIST.md +176 -0
  150. package/templates/ui-optimization-report/TPL-UI-OPTIM-REPORT.md +343 -0
@@ -0,0 +1,216 @@
1
+ /**
2
+ * 自动修复(A 类:CSS 可覆盖 + template 属性补齐 + 颜色 token 化)
3
+ * 替代旧的 scripts/batch-fix.py,使用与 scanner 一致的容差逻辑
4
+ *
5
+ * 修复项:
6
+ * - el-table-column: 缺 align="center" → 添加
7
+ * - el-input / el-select: 缺 size="small" → 添加
8
+ * - el-date-picker: 缺 style="width:100%" → 添加
9
+ * - el-table: 缺 empty-text="暂无数据" → 添加
10
+ * - <style>/<template> 内可映射 hex 颜色 → 替换为 var(--el-color-*)
11
+ */
12
+ import { readFileSync, writeFileSync, readdirSync } from "node:fs";
13
+ import { join, relative, resolve } from "node:path";
14
+ import { createSnapshot } from "./snapshot.mjs";
15
+ import { TOKEN_MAP } from "./rules/_shared.mjs";
16
+
17
+ const FIXES = {
18
+ "el-input": [{ attr: "size", value: "small" }],
19
+ "el-select": [{ attr: "size", value: "small" }],
20
+ "el-date-picker": [{ attr: "style", value: "width:100%" }],
21
+ "el-table": [{ attr: "empty-text", value: "暂无数据" }],
22
+ "el-table-column": [{ attr: "align", value: "center" }],
23
+ };
24
+
25
+ function parseTag(content, pos, tagName) {
26
+ let i = pos + tagName.length + 1;
27
+ let inSingle = false,
28
+ inDouble = false;
29
+ while (i < content.length) {
30
+ const ch = content[i];
31
+ if (ch === '"' && !inSingle) inDouble = !inDouble;
32
+ else if (ch === "'" && !inDouble) inSingle = !inSingle;
33
+ else if (!inSingle && !inDouble) {
34
+ if (content.slice(i, i + 2) === "/>")
35
+ return { text: content.slice(pos, i + 2), end: i + 2 };
36
+ if (ch === ">") return { text: content.slice(pos, i + 1), end: i + 1 };
37
+ }
38
+ i++;
39
+ }
40
+ return { text: content.slice(pos), end: content.length };
41
+ }
42
+
43
+ function addAttrIfMissing(tagText, tagName, attr, value) {
44
+ // 已存在(含动态绑定 :attr=)则跳过
45
+ const re = new RegExp(
46
+ `:?${attr.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\s*=`,
47
+ );
48
+ if (re.test(tagText)) return { text: tagText, changed: false };
49
+ const newTag = tagText.replace(
50
+ `<${tagName}`,
51
+ `<${tagName} ${attr}="${value}"`,
52
+ );
53
+ return { text: newTag, changed: true };
54
+ }
55
+
56
+ function fixTemplateAttrs(content) {
57
+ const tagPattern = new RegExp(
58
+ `<(${Object.keys(FIXES)
59
+ .sort((a, b) => b.length - a.length)
60
+ .map((t) => t.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))
61
+ .join("|")})(?=\\s|>|/>)`,
62
+ "g",
63
+ );
64
+ let result = "";
65
+ let pos = 0;
66
+ let changes = 0;
67
+ let m;
68
+ // 按位置遍历,配合 parseTag 处理嵌套引号
69
+ while (true) {
70
+ tagPattern.lastIndex = pos;
71
+ m = tagPattern.exec(content);
72
+ if (!m) {
73
+ result += content.slice(pos);
74
+ break;
75
+ }
76
+ result += content.slice(pos, m.index);
77
+ const tagName = m[1];
78
+ let { text, end } = parseTag(content, m.index, tagName);
79
+ for (const { attr, value } of FIXES[tagName]) {
80
+ const r = addAttrIfMissing(text, tagName, attr, value);
81
+ text = r.text;
82
+ if (r.changed) changes++;
83
+ }
84
+ result += text;
85
+ pos = end;
86
+ }
87
+ return { content: result, changes };
88
+ }
89
+
90
+ function fixHexColors(content) {
91
+ let changes = 0;
92
+ // 匹配 <style> 块和 template 中的 color="" / style="" 属性
93
+ let result = content.replace(
94
+ /#([0-9a-fA-F]{6}|[0-9a-fA-F]{3})\b/g,
95
+ (match) => {
96
+ const hex = match.toLowerCase();
97
+ const repl = TOKEN_MAP[hex];
98
+ if (!repl) return match;
99
+ changes++;
100
+ return repl;
101
+ },
102
+ );
103
+ // 但仅在 <style> 块和 attribute 值中替换太激进,需要限制:
104
+ // 改为:只替换出现在 <style>...</style> 内、color="..."、style="..."、:style="..." 中
105
+ // 重新实现:
106
+ changes = 0;
107
+ result = content;
108
+
109
+ // Style 块内
110
+ result = result.replace(/<style[^>]*>([\s\S]*?)<\/style>/g, (full, body) => {
111
+ const newBody = body.replace(
112
+ /#([0-9a-fA-F]{6}|[0-9a-fA-F]{3})\b/g,
113
+ (match) => {
114
+ const hex = match.toLowerCase();
115
+ if (TOKEN_MAP[hex]) {
116
+ changes++;
117
+ return TOKEN_MAP[hex];
118
+ }
119
+ return match;
120
+ },
121
+ );
122
+ return full.replace(body, newBody);
123
+ });
124
+
125
+ // template 中的 color="#xxx" / color='#xxx'
126
+ result = result.replace(
127
+ /(\bcolor\s*=\s*)(["'])(#[0-9a-fA-F]{3,8})\2/g,
128
+ (full, pre, q, hex) => {
129
+ const lo = hex.toLowerCase();
130
+ if (TOKEN_MAP[lo]) {
131
+ changes++;
132
+ return `${pre}${q}${TOKEN_MAP[lo]}${q}`;
133
+ }
134
+ return full;
135
+ },
136
+ );
137
+
138
+ return { content: result, changes };
139
+ }
140
+
141
+ function* walkVue(dir, excludes) {
142
+ for (const e of readdirSync(dir, { withFileTypes: true })) {
143
+ if (e.isDirectory()) {
144
+ if (excludes.some((x) => e.name === x)) continue;
145
+ yield* walkVue(join(dir, e.name), excludes);
146
+ } else if (e.name.endsWith(".vue")) {
147
+ yield join(dir, e.name);
148
+ }
149
+ }
150
+ }
151
+
152
+ /**
153
+ * 执行修复
154
+ * @param {object} opts { target, exclude, dryRun, projectRoot, noSnapshot }
155
+ * @returns {{ totalFiles, changedFiles, totalChanges, snapshotId? }}
156
+ */
157
+ export function runFix({
158
+ target,
159
+ exclude = ["node_modules", "dist", ".git", "SelectPopupCom"],
160
+ dryRun = false,
161
+ projectRoot,
162
+ noSnapshot = false,
163
+ }) {
164
+ const changedFiles = [];
165
+ const changedAbsPaths = [];
166
+ let totalChanges = 0;
167
+ let totalFiles = 0;
168
+
169
+ // 第一遍:收集所有需要改动的文件及其内容
170
+ const pending = [];
171
+ for (const filePath of walkVue(target, exclude)) {
172
+ totalFiles++;
173
+ const original = readFileSync(filePath, "utf8");
174
+ let content = original;
175
+ let changes = 0;
176
+ const r1 = fixTemplateAttrs(content);
177
+ content = r1.content;
178
+ changes += r1.changes;
179
+ const r2 = fixHexColors(content);
180
+ content = r2.content;
181
+ changes += r2.changes;
182
+ if (changes > 0 && content !== original) {
183
+ pending.push({ filePath, content, changes });
184
+ changedAbsPaths.push(filePath);
185
+ }
186
+ }
187
+
188
+ // 创建快照(fix 前保存原始内容)
189
+ let snapshotId = null;
190
+ if (!dryRun && !noSnapshot && changedAbsPaths.length > 0) {
191
+ const root = projectRoot || resolve(target, "..");
192
+ try {
193
+ const snap = createSnapshot({
194
+ projectRoot: root,
195
+ targetDir: target,
196
+ filePaths: changedAbsPaths,
197
+ command: "fix",
198
+ });
199
+ snapshotId = snap.id;
200
+ } catch {
201
+ /* snapshot 失败不阻断 fix */
202
+ }
203
+ }
204
+
205
+ // 第二遍:写入文件
206
+ for (const { filePath, content, changes } of pending) {
207
+ if (!dryRun) writeFileSync(filePath, content, "utf8");
208
+ changedFiles.push({
209
+ file: relative(target, filePath).replace(/\\/g, "/"),
210
+ changes,
211
+ });
212
+ totalChanges += changes;
213
+ }
214
+
215
+ return { totalFiles, changedFiles, totalChanges, snapshotId };
216
+ }
@@ -0,0 +1,403 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * wk-skills-ui — UI 风格对齐扫描器(CLI)
4
+ *
5
+ * 用法:
6
+ * wk-scan scan --target <path> # 风格扫描
7
+ * wk-scan scan --target <path> --outFile report.md
8
+ * wk-scan scan --target <path> --output json
9
+ * wk-scan check --project <path> # 接入完整性检查
10
+ * wk-scan fix --target <path> # 自动修复 A 类问题
11
+ * wk-scan fix --target <path> --dry-run
12
+ * wk-scan all --project <path> # 接入检查 + 风格扫描 + 报告
13
+ * wk-scan init # 打印接入指引
14
+ * wk-scan snapshot list # 列出快照
15
+ * wk-scan snapshot rollback [--id <id>] # 回退到快照(默认最新)
16
+ * wk-scan snapshot diff [--id <id>] # 查看快照与当前差异
17
+ * wk-scan snapshot clean [--keep <N>] # 清理旧快照
18
+ *
19
+ * 兼容旧用法:
20
+ * wk-scan --target <path> # 等价于 scan
21
+ */
22
+ import { readFileSync, readdirSync, writeFileSync } from "node:fs";
23
+ import { join, resolve, relative } from "node:path";
24
+ import { parseArgs } from "node:util";
25
+ import { getRules } from "./rules/index.mjs";
26
+ const rules = getRules();
27
+ import { generateReport } from "./report.mjs";
28
+ import { checkIntegration } from "./integration.mjs";
29
+ import { runFix } from "./fix.mjs";
30
+ import { createCoverageCollector, recommendFlows } from "./coverage.mjs";
31
+ import {
32
+ listSnapshots,
33
+ rollbackSnapshot,
34
+ diffSnapshot,
35
+ cleanSnapshots,
36
+ } from "./snapshot.mjs";
37
+ import { loadExemptConfig } from "./exempt.mjs";
38
+
39
+ const args = process.argv.slice(2);
40
+ const SUBCOMMANDS = new Set([
41
+ "scan",
42
+ "check",
43
+ "fix",
44
+ "all",
45
+ "init",
46
+ "snapshot",
47
+ ]);
48
+ let subcommand = "scan";
49
+ if (args.length > 0 && SUBCOMMANDS.has(args[0])) {
50
+ subcommand = args.shift();
51
+ }
52
+
53
+ const { values } = parseArgs({
54
+ args,
55
+ options: {
56
+ target: { type: "string", default: "./src" },
57
+ project: { type: "string", default: "." },
58
+ output: { type: "string", default: "markdown" },
59
+ exclude: { type: "string", default: "node_modules,dist,.git" },
60
+ outFile: { type: "string", default: "" },
61
+ "dry-run": { type: "boolean", default: false },
62
+ "fail-on-error": { type: "boolean", default: false },
63
+ layer: { type: "string", default: "" },
64
+ vendor: { type: "string", default: "" },
65
+ mode: { type: "string", default: "" },
66
+ id: { type: "string", default: "" },
67
+ keep: { type: "string", default: "5" },
68
+ "no-snapshot": { type: "boolean", default: false },
69
+ exempt: { type: "string", default: "" },
70
+ },
71
+ strict: false,
72
+ });
73
+
74
+ // ── 公共:扫描逻辑 ──────────────────────────────────────────────────────
75
+ function extractBlock(content, tag) {
76
+ const re = new RegExp(`(<${tag}[^>]*>)([\\s\\S]*?)(</${tag}>)`);
77
+ const match = content.match(re);
78
+ if (!match) return null;
79
+ const lineOffset = content
80
+ .slice(0, content.indexOf(match[0]))
81
+ .split("\n").length;
82
+ return { text: match[2], lineOffset };
83
+ }
84
+ function extractTemplate(content) {
85
+ const m = content.match(/(<template[^>]*>)([\s\S]*?)(<\/template>)/);
86
+ if (!m) return { text: content, lineOffset: 0 };
87
+ const lineOffset =
88
+ content.slice(0, content.indexOf(m[0])).split("\n").length - 1;
89
+ return { text: m[0], lineOffset };
90
+ }
91
+
92
+ function* walkVue(dir, excludeDirs) {
93
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
94
+ if (entry.isDirectory()) {
95
+ if (excludeDirs.some((ex) => entry.name === ex)) continue;
96
+ yield* walkVue(join(dir, entry.name), excludeDirs);
97
+ } else if (entry.name.endsWith(".vue")) {
98
+ yield join(dir, entry.name);
99
+ }
100
+ }
101
+ }
102
+
103
+ function checkScriptOps(content, relPath) {
104
+ const issues = [];
105
+ const lines = content.split("\n");
106
+ lines.forEach((line, idx) => {
107
+ if (/^\s*operations:\s*\[/.test(line) && !line.trim().startsWith("//")) {
108
+ issues.push({
109
+ file: relPath,
110
+ line: idx + 1,
111
+ rule: "R013",
112
+ category: "button",
113
+ severity: "error",
114
+ description: "columnsDef 使用旧格式 operations: [...] 文字按钮",
115
+ suggestion:
116
+ "改为 defaultSlot: ({ row }) => renderOps([...]) 图标按钮系统",
117
+ });
118
+ }
119
+ });
120
+ return issues;
121
+ }
122
+
123
+ function runScan(targetDir, excludeDirs, exemptConfig) {
124
+ const allIssues = [];
125
+ const exemptedIssues = [];
126
+ let fileCount = 0;
127
+ let exemptFileCount = 0;
128
+ const exempt = exemptConfig || { isExempt: () => false };
129
+ const coverage = createCoverageCollector();
130
+
131
+ for (const filePath of walkVue(targetDir, excludeDirs)) {
132
+ fileCount++;
133
+ const content = readFileSync(filePath, "utf8");
134
+ const { text: template, lineOffset } = extractTemplate(content);
135
+ const relPath = relative(targetDir, filePath).replace(/\\/g, "/");
136
+ coverage.addFile(relPath, template, content);
137
+
138
+ // 整文件豁免
139
+ if (exempt.isExempt(relPath)) {
140
+ exemptFileCount++;
141
+ continue;
142
+ }
143
+
144
+ const fileIssues = [];
145
+
146
+ for (const rule of rules) {
147
+ if (typeof rule.check === "function") {
148
+ fileIssues.push(...rule.check(template, relPath, lineOffset));
149
+ }
150
+ }
151
+
152
+ const styleBlock = extractBlock(content, "style");
153
+ if (styleBlock) {
154
+ for (const rule of rules) {
155
+ if (typeof rule.checkStyle === "function") {
156
+ fileIssues.push(
157
+ ...rule.checkStyle(styleBlock.text, relPath, styleBlock.lineOffset),
158
+ );
159
+ }
160
+ }
161
+ }
162
+
163
+ const scriptBlock = extractBlock(content, "script");
164
+ if (scriptBlock) {
165
+ for (const rule of rules) {
166
+ if (typeof rule.checkScript === "function") {
167
+ fileIssues.push(
168
+ ...rule.checkScript(
169
+ scriptBlock.text,
170
+ relPath,
171
+ scriptBlock.lineOffset,
172
+ ),
173
+ );
174
+ }
175
+ }
176
+ }
177
+
178
+ fileIssues.push(...checkScriptOps(content, relPath));
179
+
180
+ // 规则级豁免过滤
181
+ for (const issue of fileIssues) {
182
+ if (exempt.isExempt(relPath, issue.rule)) {
183
+ exemptedIssues.push({ ...issue, exempted: true });
184
+ } else {
185
+ allIssues.push(issue);
186
+ }
187
+ }
188
+ }
189
+ return {
190
+ allIssues,
191
+ exemptedIssues,
192
+ fileCount,
193
+ exemptFileCount,
194
+ coverage: coverage.result(),
195
+ };
196
+ }
197
+
198
+ // ── 公共:按 layer / vendor / mode 过滤 ─────────────────────────────────────
199
+ function applyFilters(issues) {
200
+ let out = issues;
201
+ if (values.layer) {
202
+ const allow = new Set(values.layer.split(",").map((s) => s.trim()));
203
+ out = out.filter((i) => allow.has(i.layer));
204
+ }
205
+ if (values.vendor) {
206
+ const allow = new Set(values.vendor.split(",").map((s) => s.trim()));
207
+ out = out.filter((i) => i.vendor && allow.has(i.vendor));
208
+ }
209
+ if (values.mode === "skin") {
210
+ // 化妆模式:只关注 L0/L1/L2,排除 L3/L4
211
+ out = out.filter((i) => ["L0", "L1", "L2"].includes(i.layer));
212
+ } else if (values.mode === "native") {
213
+ // 原生模式:全部 layer
214
+ }
215
+ return out;
216
+ }
217
+
218
+ // ── 子命令分发 ──────────────────────────────────────────────────────────
219
+ const excludeDirs = values.exclude.split(",").map((s) => s.trim());
220
+
221
+ if (subcommand === "init") {
222
+ console.log(`# wk-skills-ui 接入指引
223
+
224
+ 1. 安装依赖:
225
+ pnpm add @agile-team/wk-skills-ui
226
+
227
+ 2. 在 index.html <head> 最先加载 tokens.css:
228
+ <link rel="stylesheet" href="/node_modules/@agile-team/wk-skills-ui/design/tokens/base.css" />
229
+
230
+ 3. 在全局 SCSS 入口(如 src/assets/style/main.scss)追加:
231
+ @use '@agile-team/wk-skills-ui/styles' as *;
232
+ // 化妆模式:@use '@agile-team/wk-skills-ui/styles/presets/skin' as *;
233
+
234
+ 4. 在 src/main.ts 中注册 runtime(可选,仅当业务侧用到 defineColumns):
235
+ import { installCommonPreset } from '@agile-team/wk-skills-ui/runtime/common-preset';
236
+ installCommonPreset();
237
+
238
+ 5. 业务列定义改用 defineColumns:
239
+ import { defineColumns, renderOps } from '@agile-team/wk-skills-ui/runtime';
240
+
241
+ 6. 验证:
242
+ npx wk-ui check --project .
243
+ npx wk-ui scan --target src
244
+ `);
245
+ process.exit(0);
246
+ }
247
+
248
+ if (subcommand === "check") {
249
+ const projectRoot = resolve(values.project);
250
+ const checks = checkIntegration(projectRoot);
251
+ if (values.output === "json") {
252
+ console.log(JSON.stringify({ projectRoot, checks }, null, 2));
253
+ } else {
254
+ console.log(`# wk-skills-ui 接入完整性检查\n`);
255
+ console.log(`项目根目录:${projectRoot}\n`);
256
+ for (const c of checks) {
257
+ const icon = c.ok ? "✅" : c.severity === "error" ? "❌" : "⚠️";
258
+ console.log(`${icon} ${c.id} — ${c.description}`);
259
+ if (!c.ok && c.suggestion) console.log(` → ${c.suggestion}`);
260
+ }
261
+ }
262
+ const hasError = checks.some((c) => !c.ok && c.severity === "error");
263
+ process.exit(values["fail-on-error"] && hasError ? 1 : 0);
264
+ }
265
+
266
+ if (subcommand === "snapshot") {
267
+ const projectRoot = resolve(values.project);
268
+ const sub = args[0] || "list";
269
+ if (sub === "list") {
270
+ const snaps = listSnapshots(projectRoot);
271
+ if (snaps.length === 0) {
272
+ console.log("暂无快照。");
273
+ process.exit(0);
274
+ }
275
+ console.log(`# 快照列表(共 ${snaps.length} 个)\n`);
276
+ for (const s of snaps) {
277
+ console.log(
278
+ ` ${s.id} ${s.createdAt} ${s.command} ${s.totalFiles} 个文件 ${s.targetDir}`,
279
+ );
280
+ }
281
+ } else if (sub === "rollback") {
282
+ const result = rollbackSnapshot(
283
+ projectRoot,
284
+ values.id || undefined,
285
+ values["dry-run"],
286
+ );
287
+ const mode = values["dry-run"] ? "[DRY-RUN] " : "";
288
+ console.log(
289
+ `${mode}回退快照 ${result.snapshotId}:还原 ${result.restoredFiles.length} 个文件`,
290
+ );
291
+ if (result.skippedFiles.length > 0) {
292
+ console.log(` 跳过(文件已删除):${result.skippedFiles.join(", ")}`);
293
+ }
294
+ for (const f of result.restoredFiles) console.log(` ✔ ${f}`);
295
+ } else if (sub === "diff") {
296
+ const diffs = diffSnapshot(projectRoot, values.id || undefined);
297
+ for (const d of diffs) {
298
+ const icon =
299
+ d.status === "changed" ? "🔸" : d.status === "unchanged" ? "✅" : "❌";
300
+ console.log(` ${icon} ${d.file} — ${d.status}`);
301
+ }
302
+ } else if (sub === "clean") {
303
+ const removed = cleanSnapshots(projectRoot, parseInt(values.keep) || 5);
304
+ console.log(`清理完成,删除 ${removed} 个旧快照。`);
305
+ }
306
+ process.exit(0);
307
+ }
308
+
309
+ if (subcommand === "fix") {
310
+ const targetDir = resolve(values.target);
311
+ const projectRoot = resolve(values.project);
312
+ const result = runFix({
313
+ target: targetDir,
314
+ exclude: excludeDirs,
315
+ dryRun: values["dry-run"],
316
+ projectRoot,
317
+ noSnapshot: values["no-snapshot"],
318
+ });
319
+ const mode = values["dry-run"] ? "[DRY-RUN] " : "";
320
+ console.log(
321
+ `${mode}扫描 ${result.totalFiles} 个文件,修改 ${result.changedFiles.length} 个文件,共 ${result.totalChanges} 处改动:`,
322
+ );
323
+ for (const { file, changes } of result.changedFiles.sort((a, b) =>
324
+ a.file.localeCompare(b.file),
325
+ )) {
326
+ console.log(` ${file}: ${changes} 处`);
327
+ }
328
+ if (result.snapshotId) {
329
+ console.log(`\n📸 已创建快照: ${result.snapshotId}`);
330
+ console.log(
331
+ ` 回退命令: npx wk-scan snapshot rollback --id ${result.snapshotId}`,
332
+ );
333
+ }
334
+ if (values["dry-run"])
335
+ console.log(
336
+ `\n[DRY-RUN 模式] 未实际写入文件。去掉 --dry-run 后重新运行即可应用。`,
337
+ );
338
+ process.exit(0);
339
+ }
340
+
341
+ if (subcommand === "all") {
342
+ const projectRoot = resolve(values.project);
343
+ const targetDir =
344
+ values.target === "./src"
345
+ ? join(projectRoot, "src")
346
+ : resolve(values.target);
347
+ const integration = checkIntegration(projectRoot);
348
+ const exemptConfig = loadExemptConfig(
349
+ projectRoot,
350
+ values.exempt || undefined,
351
+ );
352
+ const { allIssues, exemptedIssues, fileCount, exemptFileCount, coverage } =
353
+ runScan(targetDir, excludeDirs, exemptConfig);
354
+ const filtered = applyFilters(allIssues);
355
+ const recommendations = recommendFlows({ issues: filtered, coverage });
356
+ const report = generateReport(filtered, fileCount, values.output, {
357
+ integration,
358
+ exemptFileCount,
359
+ exemptedIssueCount: exemptedIssues.length,
360
+ exemptPaths: exemptConfig.exemptPaths,
361
+ coverage,
362
+ recommendations,
363
+ });
364
+ if (values.outFile) {
365
+ writeFileSync(values.outFile, report, "utf8");
366
+ console.error(`[wk-scan] 报告已写入: ${values.outFile}`);
367
+ } else {
368
+ console.log(report);
369
+ }
370
+ const hasError =
371
+ allIssues.some((i) => i.severity === "error") ||
372
+ integration.some((c) => !c.ok && c.severity === "error");
373
+ process.exit(values["fail-on-error"] && hasError ? 1 : 0);
374
+ }
375
+
376
+ // 默认:scan(兼容旧用法)
377
+ {
378
+ const targetDir = resolve(values.target);
379
+ const projectRoot = resolve(values.project);
380
+ const exemptConfig = loadExemptConfig(
381
+ projectRoot,
382
+ values.exempt || undefined,
383
+ );
384
+ const { allIssues, exemptedIssues, fileCount, exemptFileCount, coverage } =
385
+ runScan(targetDir, excludeDirs, exemptConfig);
386
+ const filtered = applyFilters(allIssues);
387
+ const recommendations = recommendFlows({ issues: filtered, coverage });
388
+ const report = generateReport(filtered, fileCount, values.output, {
389
+ exemptFileCount,
390
+ exemptedIssueCount: exemptedIssues.length,
391
+ exemptPaths: exemptConfig.exemptPaths,
392
+ coverage,
393
+ recommendations,
394
+ });
395
+ if (values.outFile) {
396
+ writeFileSync(values.outFile, report, "utf8");
397
+ console.error(`[wk-scan] 报告已写入: ${values.outFile}`);
398
+ } else {
399
+ console.log(report);
400
+ }
401
+ const hasError = filtered.some((i) => i.severity === "error");
402
+ process.exit(values["fail-on-error"] && hasError ? 1 : 0);
403
+ }