@agile-team/wl-skills-kit 2.3.7 → 2.4.0

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 (30) hide show
  1. package/CHANGELOG.md +495 -404
  2. package/README.md +286 -261
  3. package/bin/wl-skills.js +796 -503
  4. package/docs/ai/345/205/250/346/231/257/345/210/206/346/236/220.md +144 -0
  5. package/docs/input-spec-api.md +263 -0
  6. package/docs/input-spec-detailed-design.md +238 -0
  7. package/docs/input-spec-page-spec.md +371 -0
  8. package/docs/input-spec-prototype.md +176 -0
  9. package/docs//345/205/250/347/233/230/345/210/206/346/236/220/344/270/216/346/231/272/350/203/275/344/275/223/346/220/255/345/273/272/346/214/207/345/215/227.md +267 -0
  10. package/files/.github/copilot-instructions.md +3 -3
  11. package/files/.github/guides/architecture.md +11 -11
  12. package/files/.github/guides/usage.md +5 -4
  13. package/files/.github/skills/_compat/headers/cursor-mdc.txt +1 -1
  14. package/files/.github/skills/_compat/headers/kiro.txt +1 -1
  15. package/files/.github/skills/_compat/headers/trae.txt +1 -1
  16. package/files/.github/skills/_pipeline.md +91 -0
  17. package/files/.github/skills/_registry.md +4 -2
  18. package/files/.github/skills/core/convention-audit/SKILL.md +241 -65
  19. package/files/.github/skills/core/page-codegen/SKILL.md +3 -3
  20. package/files/.github/skills/core/page-codegen/USAGE.md +1 -1
  21. package/files/.github/skills/core/page-codegen/templates/domains/_CONTRIBUTING.md +1 -1
  22. package/files/.github/skills/core/template-extract/SKILL.md +1 -1
  23. package/files/.github/skills/sync/env.local.json +20 -18
  24. package/files/.github/standards/02-code-structure.md +34 -4
  25. package/files/.github/standards/08-git.md +24 -0
  26. package/files/.github/standards/12-base-table.md +44 -0
  27. package/files/.github/standards/index.md +2 -2
  28. package/mcp/server.js +411 -330
  29. package/mcp/tools/projectTools.js +228 -0
  30. package/package.json +40 -39
package/bin/wl-skills.js CHANGED
@@ -1,503 +1,796 @@
1
- #!/usr/bin/env node
2
-
3
- /**
4
- * wl-skills-kit CLI v2.1
5
- *
6
- * 命令:
7
- * init 全量安装(默认,向后兼容)
8
- * update 增量更新(仅覆盖有变化的文件,展示 diff 摘要)
9
- * clean 构建清理(移除 AI 指令/文档/样例,保留组件和类型)
10
- * --help 帮助
11
- * --dry-run 预览模式(所有命令均支持)
12
- */
13
-
14
- const fs = require("fs");
15
- const path = require("path");
16
- const crypto = require("crypto");
17
-
18
- const FILES_DIR = path.resolve(__dirname, "..", "files");
19
- const TARGET_DIR = process.cwd();
20
- const MANIFEST_NAME = ".wl-skills-manifest.json";
21
- const MANIFEST_PATH = path.join(TARGET_DIR, MANIFEST_NAME);
22
- const PKG = require("../package.json");
23
- const args = process.argv.slice(2);
24
- const dryRun = args.includes("--dry-run");
25
- const showHelp = args.includes("--help") || args.includes("-h");
26
- const keepReports = args.includes("--keep-reports");
27
- const command = args.find((a) => !a.startsWith("-")) || "init";
28
-
29
- if (showHelp) {
30
- console.log(`
31
- wl-skills-kit v${PKG.version} AI Skill 模板包
32
-
33
- 用法:
34
- npx @agile-team/wl-skills-kit [命令] [选项]
35
-
36
- 命令:
37
- init 全量安装模板文件到当前项目(默认)
38
- update 增量更新(仅覆盖有变化的文件,展示变更摘要)
39
- clean 构建清理(移除开发期 AI 文件,保留 src/components + src/types)
40
-
41
- 选项:
42
- --dry-run 预览模式,不实际写入/删除任何文件
43
- --keep-reports clean 命令保留 .github/reports/(默认一起删除)
44
- --help 显示帮助
45
-
46
- 示例:
47
- npx @agile-team/wl-skills-kit 安装全量文件
48
- npx @agile-team/wl-skills-kit update 仅更新有变化的文件
49
- npx @agile-team/wl-skills-kit clean 清理开发期文件
50
- npx @agile-team/wl-skills-kit clean --keep-reports 保留 reports/中的菜单/字典数据
51
- npx @agile-team/wl-skills-kit clean --dry-run 预览将要清理哪些文件
52
-
53
- 保护路径(init / update 不覆盖已存在的):
54
- .github/reports/ AI 生成报告(团队累积数据,存在则跳过)
55
-
56
- 清理保护路径(clean 不删除):
57
- src/components/ 通用组件(被业务页面 import,构建必需)
58
- src/types/ 类型桶文件(构建必需)
59
- `);
60
- process.exit(0);
61
- }
62
-
63
- /**
64
- * 递归遍历目录,返回所有文件的相对路径(正斜杠)
65
- */
66
- function walkDir(dir, baseDir, fileList) {
67
- fileList = fileList || [];
68
- const entries = fs.readdirSync(dir, { withFileTypes: true });
69
- for (const entry of entries) {
70
- const fullPath = path.join(dir, entry.name);
71
- if (entry.isDirectory()) {
72
- walkDir(fullPath, baseDir, fileList);
73
- } else {
74
- fileList.push(path.relative(baseDir, fullPath).replace(/\\/g, "/"));
75
- }
76
- }
77
- return fileList;
78
- }
79
-
80
- /** 计算文件 md5 */
81
- function fileMd5(fp) {
82
- return crypto.createHash("md5").update(fs.readFileSync(fp)).digest("hex");
83
- }
84
-
85
- /** 计算字符串内容 md5 */
86
- function contentMd5(c) {
87
- return crypto.createHash("md5").update(c, "utf8").digest("hex");
88
- }
89
-
90
- /**
91
- * 写入文件(自动创建目录)
92
- * @returns {"created"|"updated"}
93
- */
94
- function writeFile(destPath, content) {
95
- const destDir = path.dirname(destPath);
96
- if (!fs.existsSync(destDir)) {
97
- fs.mkdirSync(destDir, { recursive: true });
98
- }
99
- const exists = fs.existsSync(destPath);
100
- if (!dryRun) {
101
- fs.writeFileSync(destPath, content, "utf8");
102
- }
103
- return exists ? "updated" : "created";
104
- }
105
-
106
- /** 复制文件(自动创建目录) */
107
- function copyFileSafe(srcPath, destPath) {
108
- const destDir = path.dirname(destPath);
109
- if (!fs.existsSync(destDir)) {
110
- fs.mkdirSync(destDir, { recursive: true });
111
- }
112
- const exists = fs.existsSync(destPath);
113
- if (!dryRun) {
114
- fs.copyFileSync(srcPath, destPath);
115
- }
116
- return exists ? "updated" : "created";
117
- }
118
-
119
- /** 删除文件,并向上清理空父目录 */
120
- function removeFileAndEmptyParents(filePath) {
121
- if (fs.existsSync(filePath)) fs.unlinkSync(filePath);
122
- let dir = path.dirname(filePath);
123
- while (dir !== TARGET_DIR && dir.length > TARGET_DIR.length) {
124
- try {
125
- if (fs.readdirSync(dir).length === 0) {
126
- fs.rmdirSync(dir);
127
- dir = path.dirname(dir);
128
- } else {
129
- break;
130
- }
131
- } catch (e) {
132
- break;
133
- }
134
- }
135
- }
136
-
137
- /** 读取 manifest */
138
- function readManifest() {
139
- if (fs.existsSync(MANIFEST_PATH)) {
140
- try {
141
- return JSON.parse(fs.readFileSync(MANIFEST_PATH, "utf8"));
142
- } catch (e) {
143
- return null;
144
- }
145
- }
146
- return null;
147
- }
148
-
149
- /** 写入 manifest */
150
- function writeManifest(data) {
151
- fs.writeFileSync(MANIFEST_PATH, JSON.stringify(data, null, 2), "utf8");
152
- }
153
-
154
- // 受保护路径(clean 不删除)
155
- const PROTECTED_PREFIXES = ["src/components/", "src/types/"];
156
- function isProtected(relPath) {
157
- return PROTECTED_PREFIXES.some((p) => relPath.startsWith(p));
158
- }
159
-
160
- // reports/ 中的 AI 生成报告:init/update 遇到已存在不覆盖(团队累积数据)
161
- function isReportFile(relPath) {
162
- return relPath.startsWith(".github/reports/") && relPath.endsWith(".md");
163
- }
164
-
165
- // ─── 旧版遗留路径(v1.x/v2.0 → v2.1 迁移清理)───────────────────────────
166
- // update 时自动检测并移除,避免旧结构与新结构并存产生歧义。
167
- const LEGACY_PATHS = [
168
- // Skill 目录重组:flat → core/sync/ops 分级(v2.1)
169
- ".github/skills/prototype-scan/SKILL.md",
170
- ".github/skills/api-contract/SKILL.md",
171
- ".github/skills/page-codegen/SKILL.md",
172
- ".github/skills/page-codegen/TPL-LIST.md",
173
- ".github/skills/page-codegen/TPL-MASTER-DETAIL.md",
174
- ".github/skills/page-codegen/TPL-TREE-LIST.md",
175
- ".github/skills/page-codegen/TPL-DETAIL-TABS.md",
176
- ".github/skills/page-codegen/TPL-FORM-ROUTE.md",
177
- ".github/skills/page-codegen/TPL-CHANGE-HISTORY.md",
178
- ".github/skills/page-codegen/TPL-RECORD-FORM.md",
179
- ".github/skills/page-codegen/TPL-DRIVEN.md",
180
- ".github/skills/page-codegen/TPL-OPERATION-STATION.md",
181
- ".github/skills/menu-sync/SKILL.md",
182
- ".github/skills/menu-sync/env/env.local.json",
183
- ".github/skills/menu-sync/env/guide.md",
184
- ".github/skills/convention-extract/SKILL.md", // 已更名为 convention-audit
185
- // docs/ 废弃文件:内容已迁移至 guides/ 或 reports/(v2.0)
186
- ".github/docs/menu-sync-design.md",
187
- ".github/docs/use-skill.md",
188
- ".github/docs/wl-skills-kit.md",
189
- ".github/docs/SYS_MENU_INFO.md", // 已迁移至 reports/
190
- // _compat/ 旧说明文件(v2.0 → v2.1 重构为可执行配置层)
191
- ".github/skills/_compat/ai-model-matrix.md",
192
- ".github/skills/_compat/editor-setup.md",
193
- ];
194
-
195
- // ─── 编辑器配置生成(从 _compat/editors.json 读取,特化 frontmatter 注入)─────
196
-
197
- const AUTO_HEADER_NOTE =
198
- "<!-- 由 @agile-team/wl-skills-kit 自动生成。源文件:.github/copilot-instructions.md -->\n" +
199
- "<!-- 请勿手动编辑本文件,更新时重新执行:npx @agile-team/wl-skills-kit@latest update -->\n\n";
200
-
201
- function getEditorConfigs(raw) {
202
- const editorsJsonPath = path.join(
203
- FILES_DIR,
204
- ".github",
205
- "skills",
206
- "_compat",
207
- "editors.json",
208
- );
209
- const headersDir = path.join(
210
- FILES_DIR,
211
- ".github",
212
- "skills",
213
- "_compat",
214
- "headers",
215
- );
216
-
217
- if (!fs.existsSync(editorsJsonPath)) {
218
- console.warn(" ⚠ _compat/editors.json 不存在,跳过多 AI 编辑器配置生成");
219
- return [];
220
- }
221
-
222
- let registry;
223
- try {
224
- registry = JSON.parse(fs.readFileSync(editorsJsonPath, "utf8"));
225
- } catch (e) {
226
- console.warn(" ⚠ _compat/editors.json 解析失败:" + e.message);
227
- return [];
228
- }
229
-
230
- const configs = [];
231
- for (const editor of registry.editors || []) {
232
- if (editor.enabled === false) continue;
233
- // GitHub Copilot 直接使用 .github/copilot-instructions.md,不重复生成
234
- if (editor.outputPath === ".github/copilot-instructions.md") continue;
235
-
236
- const headerPath = path.join(headersDir, editor.headerFile);
237
- let header = "";
238
- if (fs.existsSync(headerPath)) {
239
- header = fs.readFileSync(headerPath, "utf8");
240
- }
241
- configs.push([editor.outputPath, header + AUTO_HEADER_NOTE + raw]);
242
- }
243
- return configs;
244
- }
245
-
246
- // ─── 命令: init / update ────────────────────────────────────────────────
247
-
248
- function runInstall(incremental) {
249
- const label = incremental ? "update" : "init";
250
- console.log("");
251
- console.log(" wl-skills-kit v" + PKG.version + " [" + label + "]");
252
- console.log(" 目标目录: " + TARGET_DIR);
253
- if (dryRun) console.log(" 模式: --dry-run(预览)");
254
- console.log("");
255
-
256
- if (!fs.existsSync(FILES_DIR)) {
257
- console.error(" ✖ files/ 目录不存在,包可能已损坏");
258
- process.exit(1);
259
- }
260
-
261
- const oldManifest = readManifest();
262
- const newManifest = { version: PKG.version, files: {} };
263
- let created = 0,
264
- updated = 0,
265
- unchanged = 0,
266
- preserved = 0;
267
-
268
- // ── Step 1: 复制 files/ 静态文件 ───────────────────
269
-
270
- const files = walkDir(FILES_DIR, FILES_DIR);
271
- if (dryRun) console.log(" [Step 1] files/ 静态文件:\n");
272
-
273
- for (const relPath of files) {
274
- const src = path.join(FILES_DIR, relPath);
275
- const dest = path.join(TARGET_DIR, relPath);
276
- const srcHash = fileMd5(src);
277
- newManifest.files[relPath] = srcHash;
278
-
279
- // reports/ 下的报告文件:已存在则跳过(保护团队累积数据)
280
- if (isReportFile(relPath) && fs.existsSync(dest)) {
281
- preserved++;
282
- if (dryRun) console.log(" 保留 " + relPath + " (reports/ 已存在)");
283
- continue;
284
- }
285
-
286
- // update 模式: 跳过内容相同的文件
287
- if (incremental && fs.existsSync(dest)) {
288
- if (srcHash === fileMd5(dest)) {
289
- unchanged++;
290
- continue;
291
- }
292
- }
293
-
294
- if (dryRun) {
295
- const exists = fs.existsSync(dest);
296
- console.log(" " + (exists ? "覆盖" : "新增") + " " + relPath);
297
- exists ? updated++ : created++;
298
- } else {
299
- copyFileSafe(src, dest) === "created" ? created++ : updated++;
300
- }
301
- }
302
-
303
- // ── Step 2: 动态生成编辑器配置文件 ────────────────────────────────
304
-
305
- const INSTRUCTIONS_SRC = path.join(
306
- FILES_DIR,
307
- ".github",
308
- "copilot-instructions.md",
309
- );
310
- if (fs.existsSync(INSTRUCTIONS_SRC)) {
311
- const raw = fs.readFileSync(INSTRUCTIONS_SRC, "utf8");
312
- const editorConfigs = getEditorConfigs(raw);
313
-
314
- if (dryRun) {
315
- console.log(
316
- "\n [Step 2] 编辑器配置文件(从 copilot-instructions.md 生成):\n",
317
- );
318
- }
319
-
320
- for (const [ecPath, ecContent] of editorConfigs) {
321
- const ecDest = path.join(TARGET_DIR, ecPath);
322
- const ecHash = contentMd5(ecContent);
323
- newManifest.files[ecPath] = ecHash;
324
-
325
- if (incremental && fs.existsSync(ecDest)) {
326
- if (ecHash === fileMd5(ecDest)) {
327
- unchanged++;
328
- continue;
329
- }
330
- }
331
-
332
- if (dryRun) {
333
- const ecExists = fs.existsSync(ecDest);
334
- console.log(
335
- " " + (ecExists ? "覆盖" : "新增") + " [编辑器] " + ecPath,
336
- );
337
- ecExists ? updated++ : created++;
338
- } else {
339
- writeFile(ecDest, ecContent) === "created" ? created++ : updated++;
340
- }
341
- }
342
- }
343
-
344
- // ── Step 3: 迁移清理(仅 update,清理旧版遗留文件)──────────────────
345
-
346
- if (incremental) {
347
- let migrated = 0;
348
- if (dryRun) console.log("\n [Step 3] 旧版遗留文件检查(迁移清理):\n");
349
- for (const legacyRel of LEGACY_PATHS) {
350
- const legacyFull = path.join(TARGET_DIR, legacyRel);
351
- if (fs.existsSync(legacyFull)) {
352
- if (dryRun) {
353
- console.log(" 迁移清理 " + legacyRel + " (旧版遗留,将被移除)");
354
- } else {
355
- removeFileAndEmptyParents(legacyFull);
356
- }
357
- migrated++;
358
- }
359
- }
360
- if (!dryRun && migrated > 0) {
361
- console.log(
362
- " 迁移: " + migrated + " 个旧版文件已移除(路径已变更,见 CHANGELOG.md)",
363
- );
364
- }
365
- if (dryRun && migrated === 0) {
366
- console.log(" (无旧版遗留文件)");
367
- }
368
- }
369
-
370
- // ── Step 4: 写 manifest ────────────────────────────────────────────
371
-
372
- if (!dryRun) writeManifest(newManifest);
373
-
374
- // ── 输出统计 ──────────────────────────────────────────────────────
375
-
376
- const total = created + updated + unchanged;
377
- if (dryRun) {
378
- console.log("");
379
- if (incremental) {
380
- console.log(
381
- " 共 " +
382
- total +
383
- " 个文件(新增 " +
384
- created +
385
- ",变更 " +
386
- updated +
387
- ",未变 " +
388
- unchanged +
389
- ")(未实际写入)",
390
- );
391
- } else {
392
- console.log(" 共 " + total + " 个文件(未实际写入)");
393
- }
394
- } else {
395
- console.log(" ✔ 完成!");
396
- if (incremental) {
397
- console.log(" 新增: " + created + " 个文件");
398
- console.log(" 更新: " + updated + " 个文件");
399
- console.log(" 未变: " + unchanged + " 个文件");
400
- if (preserved > 0)
401
- console.log(
402
- " 保留: " + preserved + " 个 reports/ 文件(团队累积数据不覆盖)",
403
- );
404
- if (oldManifest && oldManifest.version !== PKG.version) {
405
- console.log(" 版本: " + oldManifest.version + " → " + PKG.version);
406
- }
407
- } else {
408
- console.log(" 新增: " + created + " 个文件");
409
- console.log(" 覆盖: " + updated + " 个文件");
410
- if (preserved > 0)
411
- console.log(
412
- " 保留: " + preserved + " 个 reports/ 文件(团队累积数据不覆盖)",
413
- );
414
- console.log(" 总计: " + (created + updated) + " 个文件");
415
- }
416
- }
417
- console.log("");
418
- }
419
-
420
- // ─── 命令: clean ────────────────────────────────────────────────────────
421
-
422
- function runClean() {
423
- console.log("");
424
- console.log(" wl-skills-kit v" + PKG.version + " [clean]");
425
- console.log(" 目标目录: " + TARGET_DIR);
426
- if (dryRun) console.log(" 模式: --dry-run(预览)");
427
- console.log("");
428
-
429
- const manifest = readManifest();
430
- if (!manifest) {
431
- console.log(" ⚠ 未找到 " + MANIFEST_NAME);
432
- console.log(" 请先执行 npx @agile-team/wl-skills-kit init 安装一次。");
433
- console.log("");
434
- process.exit(1);
435
- }
436
-
437
- const allFiles = Object.keys(manifest.files);
438
- const toRemove = allFiles.filter((f) => {
439
- if (isProtected(f)) return false;
440
- if (keepReports && f.startsWith(".github/reports/")) return false;
441
- return true;
442
- });
443
- const toKeep = allFiles.filter((f) => {
444
- if (isProtected(f)) return true;
445
- if (keepReports && f.startsWith(".github/reports/")) return true;
446
- return false;
447
- });
448
-
449
- if (dryRun) {
450
- console.log(" 将要删除(" + toRemove.length + " 个文件):\n");
451
- for (const f of toRemove) {
452
- const exists = fs.existsSync(path.join(TARGET_DIR, f));
453
- console.log(" " + (exists ? "删除" : "跳过(不存在)") + " " + f);
454
- }
455
- console.log("\n 保留(" + toKeep.length + " 个文件):\n");
456
- for (const f of toKeep) {
457
- console.log(" 保留 " + f);
458
- }
459
- } else {
460
- let removed = 0,
461
- skipped = 0;
462
- for (const f of toRemove) {
463
- const fullPath = path.join(TARGET_DIR, f);
464
- if (fs.existsSync(fullPath)) {
465
- removeFileAndEmptyParents(fullPath);
466
- removed++;
467
- } else {
468
- skipped++;
469
- }
470
- }
471
- // 删除 manifest 自身
472
- if (fs.existsSync(MANIFEST_PATH)) fs.unlinkSync(MANIFEST_PATH);
473
-
474
- console.log(" ✔ 清理完成!");
475
- console.log(" 删除: " + removed + " 个文件");
476
- if (skipped > 0) console.log(" 跳过: " + skipped + " 个(已不存在)");
477
- if (keepReports) {
478
- console.log(
479
- " 保留: " +
480
- toKeep.length +
481
- " 个文件(src/components/ + src/types/ + .github/reports/)",
482
- );
483
- } else {
484
- console.log(
485
- " 保留: " +
486
- toKeep.length +
487
- " 个文件(src/components/ + src/types/)",
488
- );
489
- }
490
- }
491
- console.log("");
492
- }
493
-
494
- // ─── 主路由 ─────────────────────────────────────────────────────────────
495
-
496
- switch (command) {
497
- case "init": runInstall(false); break;
498
- case "update": runInstall(true); break;
499
- case "clean": runClean(); break;
500
- default:
501
- console.error(' ✖ 未知命令: "' + command + '",请使用 --help 查看可用命令');
502
- process.exit(1);
503
- }
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * wl-skills-kit CLI v2.4.0
5
+ *
6
+ * 命令:
7
+ * init 全量安装(默认,向后兼容)
8
+ * update 增量更新(仅覆盖有变化的文件,展示 diff 摘要)
9
+ * clean 构建清理(移除 AI 指令/文档/样例,保留组件和类型)
10
+ * check 环境预检(工具链 / MCP 配置 / manifest)
11
+ * diff 对比已安装文件与当前 kit 版本
12
+ * validate 静态检查 src/views 页面文件完整性
13
+ * export 导出 SYS_MENU / SYS_DICT / SYS_PERMISSION 为 xlsx
14
+ * --help 帮助
15
+ * --dry-run 预览模式(所有命令均支持)
16
+ */
17
+
18
+ const fs = require("fs");
19
+ const path = require("path");
20
+ const crypto = require("crypto");
21
+
22
+ const FILES_DIR = path.resolve(__dirname, "..", "files");
23
+ const TARGET_DIR = process.cwd();
24
+ const MANIFEST_NAME = ".wl-skills-manifest.json";
25
+ const MANIFEST_PATH = path.join(TARGET_DIR, MANIFEST_NAME);
26
+ const PKG = require("../package.json");
27
+ const args = process.argv.slice(2);
28
+ const dryRun = args.includes("--dry-run");
29
+ const showHelp = args.includes("--help") || args.includes("-h");
30
+ const keepReports = args.includes("--keep-reports");
31
+ const force = args.includes("--force");
32
+ const command = args.find((a) => !a.startsWith("-")) || "init";
33
+
34
+ if (showHelp) {
35
+ console.log(`
36
+ wl-skills-kit v${PKG.version} — AI Skill 模板包
37
+
38
+ 用法:
39
+ npx @agile-team/wl-skills-kit [命令] [选项]
40
+
41
+ 命令:
42
+ init 全量安装模板文件到当前项目(默认)
43
+ update 增量更新(仅覆盖有变化的文件,展示变更摘要)
44
+ clean 构建清理(移除开发期 AI 文件,保留 src/components + src/types)
45
+ check 环境预检(Node / 工具链 / MCP 配置 / manifest)
46
+ diff 对比已安装文件与当前 kit 版本的差异
47
+ validate 静态检查 src/views 页面文件完整性
48
+ export 导出 reports/SYS_* 数据为 xlsx
49
+
50
+ 选项:
51
+ --dry-run 预览模式,不实际写入/删除任何文件
52
+ --keep-reports clean 命令保留 .github/reports/(默认一起删除)
53
+ --force 强制执行,跳过同版本检测(忽略已安装状态)
54
+ --help 显示帮助
55
+
56
+ 示例:
57
+ npx @agile-team/wl-skills-kit 安装全量文件
58
+ npx @agile-team/wl-skills-kit update 仅更新有变化的文件
59
+ npx @agile-team/wl-skills-kit update --force 强制更新(忽略同版本检测)
60
+ npx @agile-team/wl-skills-kit check 检查本地环境
61
+ npx @agile-team/wl-skills-kit diff 查看当前项目与最新 kit 差异
62
+ npx @agile-team/wl-skills-kit validate 检查 src/views 页面文件
63
+ npx @agile-team/wl-skills-kit export 导出菜单/字典/权限 xlsx
64
+ npx @agile-team/wl-skills-kit clean 清理开发期文件
65
+ npx @agile-team/wl-skills-kit clean --keep-reports 保留 reports/中的菜单/字典数据
66
+ npx @agile-team/wl-skills-kit clean --dry-run 预览将要清理哪些文件
67
+
68
+ 保护路径(init / update 不覆盖已存在的):
69
+ .github/reports/ AI 生成报告(团队累积数据,存在则跳过)
70
+
71
+ 清理保护路径(clean 不删除):
72
+ src/components/ 通用组件(被业务页面 import,构建必需)
73
+ src/types/ 类型桶文件(构建必需)
74
+ `);
75
+ process.exit(0);
76
+ }
77
+
78
+ /**
79
+ * 递归遍历目录,返回所有文件的相对路径(正斜杠)
80
+ */
81
+ function walkDir(dir, baseDir, fileList) {
82
+ fileList = fileList || [];
83
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
84
+ for (const entry of entries) {
85
+ const fullPath = path.join(dir, entry.name);
86
+ if (entry.isDirectory()) {
87
+ walkDir(fullPath, baseDir, fileList);
88
+ } else {
89
+ fileList.push(path.relative(baseDir, fullPath).replace(/\\/g, "/"));
90
+ }
91
+ }
92
+ return fileList;
93
+ }
94
+
95
+ /** 计算文件 md5 */
96
+ function fileMd5(fp) {
97
+ return crypto.createHash("md5").update(fs.readFileSync(fp)).digest("hex");
98
+ }
99
+
100
+ /** 计算字符串内容 md5 */
101
+ function contentMd5(c) {
102
+ return crypto.createHash("md5").update(c, "utf8").digest("hex");
103
+ }
104
+
105
+ /**
106
+ * 写入文件(自动创建目录)
107
+ * @returns {"created"|"updated"}
108
+ */
109
+ function writeFile(destPath, content) {
110
+ const destDir = path.dirname(destPath);
111
+ if (!fs.existsSync(destDir)) {
112
+ fs.mkdirSync(destDir, { recursive: true });
113
+ }
114
+ const exists = fs.existsSync(destPath);
115
+ if (!dryRun) {
116
+ fs.writeFileSync(destPath, content, "utf8");
117
+ }
118
+ return exists ? "updated" : "created";
119
+ }
120
+
121
+ /** 复制文件(自动创建目录) */
122
+ function copyFileSafe(srcPath, destPath) {
123
+ const destDir = path.dirname(destPath);
124
+ if (!fs.existsSync(destDir)) {
125
+ fs.mkdirSync(destDir, { recursive: true });
126
+ }
127
+ const exists = fs.existsSync(destPath);
128
+ if (!dryRun) {
129
+ fs.copyFileSync(srcPath, destPath);
130
+ }
131
+ return exists ? "updated" : "created";
132
+ }
133
+
134
+ /** 删除文件,并向上清理空父目录 */
135
+ function removeFileAndEmptyParents(filePath) {
136
+ if (fs.existsSync(filePath)) fs.unlinkSync(filePath);
137
+ let dir = path.dirname(filePath);
138
+ while (dir !== TARGET_DIR && dir.length > TARGET_DIR.length) {
139
+ try {
140
+ if (fs.readdirSync(dir).length === 0) {
141
+ fs.rmdirSync(dir);
142
+ dir = path.dirname(dir);
143
+ } else {
144
+ break;
145
+ }
146
+ } catch (e) {
147
+ break;
148
+ }
149
+ }
150
+ }
151
+
152
+ /** 读取 manifest */
153
+ function readManifest() {
154
+ if (fs.existsSync(MANIFEST_PATH)) {
155
+ try {
156
+ return JSON.parse(fs.readFileSync(MANIFEST_PATH, "utf8"));
157
+ } catch (e) {
158
+ return null;
159
+ }
160
+ }
161
+ return null;
162
+ }
163
+
164
+ /** 写入 manifest */
165
+ function writeManifest(data) {
166
+ fs.writeFileSync(MANIFEST_PATH, JSON.stringify(data, null, 2), "utf8");
167
+ }
168
+
169
+ // 受保护路径(clean 不删除)
170
+ const PROTECTED_PREFIXES = ["src/components/", "src/types/"];
171
+ function isProtected(relPath) {
172
+ return PROTECTED_PREFIXES.some((p) => relPath.startsWith(p));
173
+ }
174
+
175
+ // reports/ 中的 AI 生成报告:init/update 遇到已存在不覆盖(团队累积数据)
176
+ function isReportFile(relPath) {
177
+ return relPath.startsWith(".github/reports/") && relPath.endsWith(".md");
178
+ }
179
+
180
+ // ─── 旧版遗留路径(v1.x/v2.0 → v2.1 迁移清理)───────────────────────────
181
+ // update 时自动检测并移除,避免旧结构与新结构并存产生歧义。
182
+ const LEGACY_PATHS = [
183
+ // Skill 目录重组:flat → core/sync/ops 分级(v2.1)
184
+ ".github/skills/prototype-scan/SKILL.md",
185
+ ".github/skills/api-contract/SKILL.md",
186
+ ".github/skills/page-codegen/SKILL.md",
187
+ ".github/skills/page-codegen/TPL-LIST.md",
188
+ ".github/skills/page-codegen/TPL-MASTER-DETAIL.md",
189
+ ".github/skills/page-codegen/TPL-TREE-LIST.md",
190
+ ".github/skills/page-codegen/TPL-DETAIL-TABS.md",
191
+ ".github/skills/page-codegen/TPL-FORM-ROUTE.md",
192
+ ".github/skills/page-codegen/TPL-CHANGE-HISTORY.md",
193
+ ".github/skills/page-codegen/TPL-RECORD-FORM.md",
194
+ ".github/skills/page-codegen/TPL-DRIVEN.md",
195
+ ".github/skills/page-codegen/TPL-OPERATION-STATION.md",
196
+ ".github/skills/menu-sync/SKILL.md",
197
+ ".github/skills/menu-sync/env/env.local.json",
198
+ ".github/skills/menu-sync/env/guide.md",
199
+ ".github/skills/convention-extract/SKILL.md", // 已更名为 convention-audit
200
+ // docs/ 废弃文件:内容已迁移至 guides/ 或 reports/(v2.0)
201
+ ".github/docs/menu-sync-design.md",
202
+ ".github/docs/use-skill.md",
203
+ ".github/docs/wl-skills-kit.md",
204
+ ".github/docs/SYS_MENU_INFO.md", // 已迁移至 reports/
205
+ // _compat/ 旧说明文件(v2.0 → v2.1 重构为可执行配置层)
206
+ ".github/skills/_compat/ai-model-matrix.md",
207
+ ".github/skills/_compat/editor-setup.md",
208
+ ];
209
+
210
+ // ─── 编辑器配置生成(从 _compat/editors.json 读取,特化 frontmatter 注入)─────
211
+
212
+ const AUTO_HEADER_NOTE =
213
+ "<!-- 由 @agile-team/wl-skills-kit 自动生成。源文件:.github/copilot-instructions.md -->\n" +
214
+ "<!-- 请勿手动编辑本文件,更新时重新执行:npx @agile-team/wl-skills-kit@latest update -->\n\n";
215
+
216
+ function getEditorConfigs(raw) {
217
+ const editorsJsonPath = path.join(
218
+ FILES_DIR,
219
+ ".github",
220
+ "skills",
221
+ "_compat",
222
+ "editors.json",
223
+ );
224
+ const headersDir = path.join(
225
+ FILES_DIR,
226
+ ".github",
227
+ "skills",
228
+ "_compat",
229
+ "headers",
230
+ );
231
+
232
+ if (!fs.existsSync(editorsJsonPath)) {
233
+ console.warn(" ⚠ _compat/editors.json 不存在,跳过多 AI 编辑器配置生成");
234
+ return [];
235
+ }
236
+
237
+ let registry;
238
+ try {
239
+ registry = JSON.parse(fs.readFileSync(editorsJsonPath, "utf8"));
240
+ } catch (e) {
241
+ console.warn(" ⚠ _compat/editors.json 解析失败:" + e.message);
242
+ return [];
243
+ }
244
+
245
+ const configs = [];
246
+ for (const editor of registry.editors || []) {
247
+ if (editor.enabled === false) continue;
248
+ // GitHub Copilot 直接使用 .github/copilot-instructions.md,不重复生成
249
+ if (editor.outputPath === ".github/copilot-instructions.md") continue;
250
+
251
+ const headerPath = path.join(headersDir, editor.headerFile);
252
+ let header = "";
253
+ if (fs.existsSync(headerPath)) {
254
+ header = fs.readFileSync(headerPath, "utf8");
255
+ }
256
+ configs.push([editor.outputPath, header + AUTO_HEADER_NOTE + raw]);
257
+ }
258
+ return configs;
259
+ }
260
+
261
+ // ─── 命令: init / update ────────────────────────────────────────────────
262
+
263
+ function runInstall(incremental) {
264
+ const label = incremental ? "update" : "init";
265
+ console.log("");
266
+ console.log(" wl-skills-kit v" + PKG.version + " [" + label + "]");
267
+ console.log(" 目标目录: " + TARGET_DIR);
268
+ if (dryRun) console.log(" 模式: --dry-run(预览)");
269
+ if (force) console.log(" 模式: --force(强制执行)");
270
+ console.log("");
271
+
272
+ if (!fs.existsSync(FILES_DIR)) {
273
+ console.error(" files/ 目录不存在,包可能已损坏");
274
+ process.exit(1);
275
+ }
276
+
277
+ const oldManifest = readManifest();
278
+
279
+ // ── 版本去重:同版本跳过,不同版本自动增量更新 ──────────────────────
280
+ if (oldManifest && !force) {
281
+ if (oldManifest.version === PKG.version) {
282
+ console.log(" ✔ 当前项目已安装 v" + PKG.version + ",无需重复操作");
283
+ console.log(
284
+ " 如需强制重装:npx @agile-team/wl-skills-kit@latest " +
285
+ label +
286
+ " --force",
287
+ );
288
+ console.log("");
289
+ return;
290
+ }
291
+ if (!incremental) {
292
+ // init 命令但已有旧版本 → 自动切换为增量更新,避免全量覆盖
293
+ console.log(
294
+ " ℹ 检测到已安装 v" + oldManifest.version + ",自动切换为增量更新模式",
295
+ );
296
+ console.log("");
297
+ incremental = true;
298
+ }
299
+ }
300
+ const newManifest = { version: PKG.version, files: {} };
301
+ let created = 0,
302
+ updated = 0,
303
+ unchanged = 0,
304
+ preserved = 0;
305
+
306
+ // ── Step 1: 复制 files/ 静态文件 ───────────────────
307
+
308
+ const files = walkDir(FILES_DIR, FILES_DIR);
309
+ if (dryRun) console.log(" [Step 1] files/ 静态文件:\n");
310
+
311
+ for (const relPath of files) {
312
+ const src = path.join(FILES_DIR, relPath);
313
+ const dest = path.join(TARGET_DIR, relPath);
314
+ const srcHash = fileMd5(src);
315
+ newManifest.files[relPath] = srcHash;
316
+
317
+ // reports/ 下的报告文件:已存在则跳过(保护团队累积数据)
318
+ if (isReportFile(relPath) && fs.existsSync(dest)) {
319
+ preserved++;
320
+ if (dryRun) console.log(" 保留 " + relPath + " (reports/ 已存在)");
321
+ continue;
322
+ }
323
+
324
+ // update 模式: 跳过内容相同的文件
325
+ if (incremental && fs.existsSync(dest)) {
326
+ if (srcHash === fileMd5(dest)) {
327
+ unchanged++;
328
+ continue;
329
+ }
330
+ }
331
+
332
+ if (dryRun) {
333
+ const exists = fs.existsSync(dest);
334
+ console.log(" " + (exists ? "覆盖" : "新增") + " " + relPath);
335
+ exists ? updated++ : created++;
336
+ } else {
337
+ copyFileSafe(src, dest) === "created" ? created++ : updated++;
338
+ }
339
+ }
340
+
341
+ // ── Step 2: 动态生成编辑器配置文件 ────────────────────────────────
342
+
343
+ const INSTRUCTIONS_SRC = path.join(
344
+ FILES_DIR,
345
+ ".github",
346
+ "copilot-instructions.md",
347
+ );
348
+ if (fs.existsSync(INSTRUCTIONS_SRC)) {
349
+ const raw = fs.readFileSync(INSTRUCTIONS_SRC, "utf8");
350
+ const editorConfigs = getEditorConfigs(raw);
351
+
352
+ if (dryRun) {
353
+ console.log(
354
+ "\n [Step 2] 编辑器配置文件(从 copilot-instructions.md 生成):\n",
355
+ );
356
+ }
357
+
358
+ for (const [ecPath, ecContent] of editorConfigs) {
359
+ const ecDest = path.join(TARGET_DIR, ecPath);
360
+ const ecHash = contentMd5(ecContent);
361
+ newManifest.files[ecPath] = ecHash;
362
+
363
+ if (incremental && fs.existsSync(ecDest)) {
364
+ if (ecHash === fileMd5(ecDest)) {
365
+ unchanged++;
366
+ continue;
367
+ }
368
+ }
369
+
370
+ if (dryRun) {
371
+ const ecExists = fs.existsSync(ecDest);
372
+ console.log(
373
+ " " + (ecExists ? "覆盖" : "新增") + " [编辑器] " + ecPath,
374
+ );
375
+ ecExists ? updated++ : created++;
376
+ } else {
377
+ writeFile(ecDest, ecContent) === "created" ? created++ : updated++;
378
+ }
379
+ }
380
+ }
381
+
382
+ // ── Step 3: 迁移清理(仅 update,清理旧版遗留文件)──────────────────
383
+
384
+ if (incremental) {
385
+ let migrated = 0;
386
+ if (dryRun) console.log("\n [Step 3] 旧版遗留文件检查(迁移清理):\n");
387
+ for (const legacyRel of LEGACY_PATHS) {
388
+ const legacyFull = path.join(TARGET_DIR, legacyRel);
389
+ if (fs.existsSync(legacyFull)) {
390
+ if (dryRun) {
391
+ console.log(" 迁移清理 " + legacyRel + " (旧版遗留,将被移除)");
392
+ } else {
393
+ removeFileAndEmptyParents(legacyFull);
394
+ }
395
+ migrated++;
396
+ }
397
+ }
398
+ if (!dryRun && migrated > 0) {
399
+ console.log(
400
+ " 迁移: " +
401
+ migrated +
402
+ " 个旧版文件已移除(路径已变更,见 CHANGELOG.md)",
403
+ );
404
+ }
405
+ if (dryRun && migrated === 0) {
406
+ console.log(" (无旧版遗留文件)");
407
+ }
408
+ }
409
+
410
+ // ── Step 4: 写 manifest ────────────────────────────────────────────
411
+
412
+ if (!dryRun) writeManifest(newManifest);
413
+
414
+ // ── 输出统计 ──────────────────────────────────────────────────────
415
+
416
+ const total = created + updated + unchanged;
417
+ if (dryRun) {
418
+ console.log("");
419
+ if (incremental) {
420
+ console.log(
421
+ " 共 " +
422
+ total +
423
+ " 个文件(新增 " +
424
+ created +
425
+ ",变更 " +
426
+ updated +
427
+ ",未变 " +
428
+ unchanged +
429
+ ")(未实际写入)",
430
+ );
431
+ } else {
432
+ console.log(" " + total + " 个文件(未实际写入)");
433
+ }
434
+ } else {
435
+ console.log(" ✔ 完成!");
436
+ if (incremental) {
437
+ console.log(" 新增: " + created + " 个文件");
438
+ console.log(" 更新: " + updated + " 个文件");
439
+ console.log(" 未变: " + unchanged + " 个文件");
440
+ if (preserved > 0)
441
+ console.log(
442
+ " 保留: " + preserved + " 个 reports/ 文件(团队累积数据不覆盖)",
443
+ );
444
+ if (oldManifest && oldManifest.version !== PKG.version) {
445
+ console.log(" 版本: " + oldManifest.version + "" + PKG.version);
446
+ }
447
+ } else {
448
+ console.log(" 新增: " + created + " 个文件");
449
+ console.log(" 覆盖: " + updated + " 个文件");
450
+ if (preserved > 0)
451
+ console.log(
452
+ " 保留: " + preserved + " 个 reports/ 文件(团队累积数据不覆盖)",
453
+ );
454
+ console.log(" 总计: " + (created + updated) + " 个文件");
455
+ }
456
+ }
457
+ console.log("");
458
+ }
459
+
460
+ // ─── 命令: clean ────────────────────────────────────────────────────────
461
+
462
+ function runClean() {
463
+ console.log("");
464
+ console.log(" wl-skills-kit v" + PKG.version + " [clean]");
465
+ console.log(" 目标目录: " + TARGET_DIR);
466
+ if (dryRun) console.log(" 模式: --dry-run(预览)");
467
+ console.log("");
468
+
469
+ const manifest = readManifest();
470
+ if (!manifest) {
471
+ console.log(" ⚠ 未找到 " + MANIFEST_NAME);
472
+ console.log(" 请先执行 npx @agile-team/wl-skills-kit init 安装一次。");
473
+ console.log("");
474
+ process.exit(1);
475
+ }
476
+
477
+ const allFiles = Object.keys(manifest.files);
478
+ const toRemove = allFiles.filter((f) => {
479
+ if (isProtected(f)) return false;
480
+ if (keepReports && f.startsWith(".github/reports/")) return false;
481
+ return true;
482
+ });
483
+ const toKeep = allFiles.filter((f) => {
484
+ if (isProtected(f)) return true;
485
+ if (keepReports && f.startsWith(".github/reports/")) return true;
486
+ return false;
487
+ });
488
+
489
+ if (dryRun) {
490
+ console.log(" 将要删除(" + toRemove.length + " 个文件):\n");
491
+ for (const f of toRemove) {
492
+ const exists = fs.existsSync(path.join(TARGET_DIR, f));
493
+ console.log(" " + (exists ? "删除" : "跳过(不存在)") + " " + f);
494
+ }
495
+ console.log("\n 保留(" + toKeep.length + " 个文件):\n");
496
+ for (const f of toKeep) {
497
+ console.log(" 保留 " + f);
498
+ }
499
+ } else {
500
+ let removed = 0,
501
+ skipped = 0;
502
+ for (const f of toRemove) {
503
+ const fullPath = path.join(TARGET_DIR, f);
504
+ if (fs.existsSync(fullPath)) {
505
+ removeFileAndEmptyParents(fullPath);
506
+ removed++;
507
+ } else {
508
+ skipped++;
509
+ }
510
+ }
511
+ // 删除 manifest 自身
512
+ if (fs.existsSync(MANIFEST_PATH)) fs.unlinkSync(MANIFEST_PATH);
513
+
514
+ console.log(" ✔ 清理完成!");
515
+ console.log(" 删除: " + removed + " 个文件");
516
+ if (skipped > 0) console.log(" 跳过: " + skipped + " 个(已不存在)");
517
+ if (keepReports) {
518
+ console.log(
519
+ " 保留: " +
520
+ toKeep.length +
521
+ " 个文件(src/components/ + src/types/ + .github/reports/)",
522
+ );
523
+ } else {
524
+ console.log(
525
+ " 保留: " +
526
+ toKeep.length +
527
+ " 个文件(src/components/ + src/types/)",
528
+ );
529
+ }
530
+ }
531
+ console.log("");
532
+ }
533
+
534
+ function expectedManifestFiles() {
535
+ const expected = {};
536
+ const files = walkDir(FILES_DIR, FILES_DIR);
537
+ for (const relPath of files) {
538
+ expected[relPath] = fileMd5(path.join(FILES_DIR, relPath));
539
+ }
540
+ const instructionsSrc = path.join(FILES_DIR, ".github", "copilot-instructions.md");
541
+ if (fs.existsSync(instructionsSrc)) {
542
+ const raw = fs.readFileSync(instructionsSrc, "utf8");
543
+ for (const [ecPath, ecContent] of getEditorConfigs(raw)) {
544
+ expected[ecPath] = contentMd5(ecContent);
545
+ }
546
+ }
547
+ return expected;
548
+ }
549
+
550
+ function statusIcon(ok) {
551
+ return ok ? "✔" : "✖";
552
+ }
553
+
554
+ function runCheck() {
555
+ console.log("");
556
+ console.log(" wl-skills-kit v" + PKG.version + " [check]");
557
+ console.log(" 目标目录: " + TARGET_DIR);
558
+ console.log("");
559
+
560
+ const checks = [];
561
+ function add(name, ok, detail) {
562
+ checks.push({ name, ok, detail });
563
+ }
564
+
565
+ const nodeMajor = Number(process.versions.node.split(".")[0]);
566
+ add("Node 版本", nodeMajor >= 16, process.versions.node + "(要求 >=16)");
567
+
568
+ const toolFiles = [".prettierrc.js", "eslint.config.ts", ".husky"];
569
+ for (const rel of toolFiles) {
570
+ add(rel, fs.existsSync(path.join(TARGET_DIR, rel)), fs.existsSync(path.join(TARGET_DIR, rel)) ? "存在" : "缺失");
571
+ }
572
+
573
+ const manifest = readManifest();
574
+ add(MANIFEST_NAME, Boolean(manifest), manifest ? "已安装 v" + manifest.version : "未安装");
575
+
576
+ const envPath = path.join(TARGET_DIR, ".github", "skills", "sync", "env.local.json");
577
+ let envOk = false;
578
+ let envDetail = "缺失";
579
+ if (fs.existsSync(envPath)) {
580
+ try {
581
+ const env = JSON.parse(fs.readFileSync(envPath, "utf8"));
582
+ const gatewayOk = env.gatewayPath && !String(env.gatewayPath).includes("你的网关");
583
+ const tokenOk = env.token && !String(env.token).includes("Bearer Token");
584
+ envOk = Boolean(gatewayOk && tokenOk);
585
+ envDetail = envOk ? "已填写 gatewayPath/token" : "存在但仍含占位值";
586
+ } catch (e) {
587
+ envDetail = "JSON 解析失败:" + e.message;
588
+ }
589
+ }
590
+ add("MCP env.local.json", envOk, envDetail);
591
+
592
+ const mcpServer = path.join(TARGET_DIR, "node_modules", "@agile-team", "wl-skills-kit", "mcp", "server.js");
593
+ add("MCP server", fs.existsSync(mcpServer) || fs.existsSync(path.join(__dirname, "..", "mcp", "server.js")), "server.js 可发现");
594
+
595
+ for (const item of checks) {
596
+ console.log(" " + statusIcon(item.ok) + " " + item.name + " — " + item.detail);
597
+ }
598
+ const failed = checks.filter((item) => !item.ok).length;
599
+ console.log("");
600
+ console.log(failed === 0 ? " ✔ 环境预检通过" : " ⚠ 环境预检完成,发现 " + failed + " 项需处理");
601
+ console.log("");
602
+ if (failed > 0) process.exitCode = 1;
603
+ }
604
+
605
+ function runDiff() {
606
+ console.log("");
607
+ console.log(" wl-skills-kit v" + PKG.version + " [diff]");
608
+ console.log(" 目标目录: " + TARGET_DIR);
609
+ console.log("");
610
+
611
+ const manifest = readManifest();
612
+ const expected = expectedManifestFiles();
613
+ const current = manifest && manifest.files ? manifest.files : {};
614
+ const added = [];
615
+ const changed = [];
616
+ const removed = [];
617
+ const same = [];
618
+
619
+ for (const relPath of Object.keys(expected).sort()) {
620
+ const target = path.join(TARGET_DIR, relPath);
621
+ if (!fs.existsSync(target)) {
622
+ added.push(relPath);
623
+ } else if (fileMd5(target) !== expected[relPath]) {
624
+ changed.push(relPath);
625
+ } else {
626
+ same.push(relPath);
627
+ }
628
+ }
629
+
630
+ for (const relPath of Object.keys(current).sort()) {
631
+ if (!expected[relPath] && fs.existsSync(path.join(TARGET_DIR, relPath))) {
632
+ removed.push(relPath);
633
+ }
634
+ }
635
+
636
+ console.log(" 当前 manifest: " + (manifest ? "v" + manifest.version : "未找到"));
637
+ console.log(" 最新 kit: v" + PKG.version);
638
+ console.log(" 新增/缺失: " + added.length);
639
+ console.log(" 内容不同: " + changed.length);
640
+ console.log(" 旧版残留: " + removed.length);
641
+ console.log(" 相同: " + same.length);
642
+ console.log("");
643
+
644
+ function printGroup(title, list) {
645
+ if (list.length === 0) return;
646
+ console.log(" " + title + ":");
647
+ for (const relPath of list.slice(0, 80)) console.log(" - " + relPath);
648
+ if (list.length > 80) console.log(" ... 还有 " + (list.length - 80) + " 项");
649
+ console.log("");
650
+ }
651
+
652
+ printGroup("新增/缺失(update 会写入)", added);
653
+ printGroup("内容不同(update 会覆盖,reports 除外)", changed);
654
+ printGroup("旧版残留(update 会迁移清理)", removed);
655
+ }
656
+
657
+ function scanPageDirs(scanRel) {
658
+ const scanDir = path.join(TARGET_DIR, scanRel || "src/views");
659
+ if (!fs.existsSync(scanDir)) return [];
660
+ const files = walkDir(scanDir, TARGET_DIR);
661
+ const dirs = new Map();
662
+ for (const rel of files) {
663
+ const dir = path.dirname(rel).replace(/\\/g, "/");
664
+ const name = path.basename(rel);
665
+ if (!dirs.has(dir)) dirs.set(dir, new Set());
666
+ dirs.get(dir).add(name);
667
+ }
668
+ const pages = [];
669
+ for (const [dir, names] of dirs.entries()) {
670
+ if (!names.has("index.vue")) continue;
671
+ let apiConfigCount = 0;
672
+ const dataPath = path.join(TARGET_DIR, dir, "data.ts");
673
+ if (fs.existsSync(dataPath)) {
674
+ apiConfigCount = (fs.readFileSync(dataPath, "utf8").match(/API_CONFIG/g) || []).length;
675
+ }
676
+ pages.push({
677
+ dir,
678
+ hasDataTs: names.has("data.ts"),
679
+ hasIndexScss: names.has("index.scss"),
680
+ hasApiMd: names.has("api.md"),
681
+ apiConfigCount,
682
+ });
683
+ }
684
+ return pages.sort((a, b) => a.dir.localeCompare(b.dir));
685
+ }
686
+
687
+ function runValidate() {
688
+ const scanPath = args.find((a) => !a.startsWith("-") && a !== command) || "src/views";
689
+ const pages = scanPageDirs(scanPath);
690
+ console.log("");
691
+ console.log(" wl-skills-kit v" + PKG.version + " [validate]");
692
+ console.log(" 扫描目录: " + scanPath);
693
+ console.log("");
694
+
695
+ if (pages.length === 0) {
696
+ console.log(" ⚠ 未发现包含 index.vue 的页面目录");
697
+ console.log("");
698
+ process.exitCode = 1;
699
+ return;
700
+ }
701
+
702
+ const issues = [];
703
+ for (const page of pages) {
704
+ if (!page.hasDataTs) issues.push({ level: "warn", dir: page.dir, text: "缺 data.ts(需结合页面复杂度判断)" });
705
+ if (!page.hasIndexScss) issues.push({ level: "warn", dir: page.dir, text: "缺 index.scss" });
706
+ if (page.apiConfigCount > 0 && !page.hasApiMd) issues.push({ level: "warn", dir: page.dir, text: "检测到 API_CONFIG 但缺 api.md" });
707
+ }
708
+
709
+ console.log(" 页面目录: " + pages.length);
710
+ console.log(" 提示项: " + issues.length);
711
+ console.log("");
712
+ for (const issue of issues) {
713
+ console.log(" ⚠ " + issue.dir + " — " + issue.text);
714
+ }
715
+ if (issues.length === 0) console.log(" ✔ 页面文件完整性检查通过");
716
+ console.log("");
717
+ if (issues.length > 0) process.exitCode = 1;
718
+ }
719
+
720
+ function parseMarkdownTable(content) {
721
+ return content
722
+ .split(/\r?\n/)
723
+ .filter((line) => /^\|.*\|$/.test(line) && !/^\|\s*-+/.test(line))
724
+ .map((line) => line.split("|").slice(1, -1).map((cell) => cell.trim()));
725
+ }
726
+
727
+ function runExport() {
728
+ console.log("");
729
+ console.log(" wl-skills-kit v" + PKG.version + " [export]");
730
+ console.log(" 目标目录: " + TARGET_DIR);
731
+ console.log("");
732
+
733
+ const reportDir = path.join(TARGET_DIR, ".github", "reports");
734
+ const files = [
735
+ ["菜单", "SYS_MENU_INFO.md"],
736
+ ["字典", "SYS_DICT_INFO.md"],
737
+ ["权限", "SYS_PERMISSION_INFO.md"],
738
+ ];
739
+ const sheets = [];
740
+ let addedSheets = 0;
741
+ for (const [sheetName, fileName] of files) {
742
+ const full = path.join(reportDir, fileName);
743
+ if (!fs.existsSync(full)) continue;
744
+ const content = fs.readFileSync(full, "utf8");
745
+ let rows = parseMarkdownTable(content);
746
+ if (rows.length === 0) rows = content.split(/\r?\n/).filter(Boolean).map((line) => [line]);
747
+ sheets.push([sheetName, rows]);
748
+ addedSheets++;
749
+ }
750
+
751
+ if (addedSheets === 0) {
752
+ console.log(" ⚠ 未找到可导出的 SYS_* 报告");
753
+ console.log("");
754
+ process.exitCode = 1;
755
+ return;
756
+ }
757
+
758
+ const outDir = path.join(reportDir, "exports");
759
+ const outFile = path.join(outDir, "wl-skills-sys-export.xlsx");
760
+ if (dryRun) {
761
+ console.log(" 将导出: " + outFile);
762
+ console.log(" sheet 数: " + addedSheets);
763
+ } else {
764
+ let XLSX;
765
+ try {
766
+ XLSX = require("xlsx");
767
+ } catch (e) {
768
+ console.error(" ✖ 未找到 xlsx 依赖,请重新安装最新 @agile-team/wl-skills-kit");
769
+ process.exit(1);
770
+ }
771
+ const wb = XLSX.utils.book_new();
772
+ for (const [sheetName, rows] of sheets) {
773
+ XLSX.utils.book_append_sheet(wb, XLSX.utils.aoa_to_sheet(rows), sheetName);
774
+ }
775
+ fs.mkdirSync(outDir, { recursive: true });
776
+ XLSX.writeFile(wb, outFile);
777
+ console.log(" ✔ 已导出: " + outFile);
778
+ console.log(" sheet 数: " + addedSheets);
779
+ }
780
+ console.log("");
781
+ }
782
+
783
+ // ─── 主路由 ─────────────────────────────────────────────────────────────
784
+
785
+ switch (command) {
786
+ case "init": runInstall(false); break;
787
+ case "update": runInstall(true); break;
788
+ case "clean": runClean(); break;
789
+ case "check": runCheck(); break;
790
+ case "diff": runDiff(); break;
791
+ case "validate": runValidate(); break;
792
+ case "export": runExport(); break;
793
+ default:
794
+ console.error(' ✖ 未知命令: "' + command + '",请使用 --help 查看可用命令');
795
+ process.exit(1);
796
+ }