@qilitt-mickey/vue3-temp-skill 1.2.5 → 1.2.6

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.
package/bin/cli.js CHANGED
@@ -31,11 +31,18 @@ const SKILL_DIR = path.resolve(__dirname, "..");
31
31
  const SKILL_NAME = "vue3-temp-skill";
32
32
  const TRACK_FILE = path.join(HOME, ".vue3-temp-skill.json");
33
33
  // 只复制 AI 运行时真正需要的文件;validate/cli 属作者侧词典维护工具,不随技能分发。
34
- // design-audit.mjs / check.mjs / apply-final-gate.mjs 必须随包分发:E11 交付完整性 / E12 设计层
35
- // 累积性(ledger 台账)/ 终态机读凭证(四闸串联,FINAL-GATE: PASS)是落地会话的运行时闸——
36
- // 缺了它们「只挑主题令牌落地 + 覆盖历史成果 + 以自述结束会话」无法被静态拦截
37
- // (失效模式:运行时副本缺审计闸时,残缺口变更集会一路绿灯)。
38
- const RUNTIME_SCRIPT_FILES = ["match.py", "modules.json", "check-landing.mjs"];
34
+ // 施工脚本必须随包分发:generate-css.mjs(契约→CSS 确定性编译)、generate-page.mjs
35
+ // (契约 structure→页面脚手架)、verify-render.mjs(Playwright 渲染验收)、
36
+ // check-landing.mjs(落地八项自检)。缺了它们,弱模型被指示跑脚本时文件不存在,
37
+ // 只能手抄 CSS 或直接跳过(失效模式:内容区空白、主题布局未落地)。
38
+ const RUNTIME_SCRIPT_FILES = [
39
+ "match.py",
40
+ "modules.json",
41
+ "check-landing.mjs",
42
+ "generate-css.mjs",
43
+ "generate-page.mjs",
44
+ "verify-render.mjs",
45
+ ];
39
46
  const PKG_VERSION = JSON.parse(fs.readFileSync(path.join(SKILL_DIR, "package.json"), "utf-8")).version;
40
47
 
41
48
  // ============================================================
@@ -80,6 +87,11 @@ const TARGETS = {
80
87
  dir: (p) => (p ? path.join(p, ".trae/rules") : path.join(HOME, ".trae/rules")),
81
88
  type: "rules",
82
89
  },
90
+ codebuddy: {
91
+ name: "CodeBuddy",
92
+ dir: (p) => (p ? path.join(p, ".codebuddy/rules") : path.join(HOME, ".codebuddy/rules")),
93
+ type: "rules",
94
+ },
83
95
  claude: {
84
96
  name: "Claude Code",
85
97
  dir: (p) => (p ? path.join(p, ".claude/skills") : path.join(HOME, ".claude/skills")),
@@ -222,32 +234,15 @@ function recordUninstall(targetKey, projectDir) {
222
234
  // ============================================================
223
235
 
224
236
  /**
225
- * rules 类型:Windsurf / Trae
226
- */
227
- function installRules(targetKey, projectDir) {
228
- const cfg = TARGETS[targetKey];
229
- const targetDir = cfg.dir(projectDir);
230
- ensureDir(targetDir);
231
-
232
- const content = readFullSkill();
233
- const file = path.join(targetDir, `${SKILL_NAME}.md`);
234
- fs.writeFileSync(file, content, "utf-8");
235
- }
236
-
237
- /**
238
- * skills 类型:Cursor / Qoder / Claude / Universal
237
+ * 复制技能运行时本体(SKILL.md / README.md / references / scripts / adapters / mapping)
238
+ * 供 skills 与 rules 两种形态共用——rules 形态也必须落盘本体,
239
+ * 否则规则文件指示的施工脚本不存在。
239
240
  */
240
- function installSkills(targetKey, projectDir) {
241
- const cfg = TARGETS[targetKey];
242
- const targetDir = cfg.dir(projectDir);
243
- const dest = path.join(targetDir, SKILL_NAME);
244
-
241
+ function copySkillBody(dest) {
245
242
  // 先清理旧版本
246
243
  if (fs.existsSync(dest)) {
247
244
  removeDir(dest);
248
245
  }
249
-
250
- // 只复制必要文件,不复制整个包目录(避免缓存目录中的无关文件导致递归)
251
246
  ensureDir(dest);
252
247
  copyFile(path.join(SKILL_DIR, "SKILL.md"), path.join(dest, "SKILL.md"));
253
248
  copyFile(path.join(SKILL_DIR, "README.md"), path.join(dest, "README.md"));
@@ -266,11 +261,57 @@ function installSkills(targetKey, projectDir) {
266
261
  }
267
262
  }
268
263
 
269
- // 视觉契约落地依赖:mapping/ 三列变量映射表(区域名 ↔ 项目现状对照)
264
+ // 契约→框架等价转换 adapter(generate-css / verify-render 的运行时依赖)
265
+ const adaptersSrc = path.join(SKILL_DIR, "adapters");
266
+ if (fs.existsSync(adaptersSrc)) {
267
+ copyDir(adaptersSrc, path.join(dest, "adapters"));
268
+ } else {
269
+ console.log(
270
+ yellow(
271
+ ` ! 技能包缺少 adapters/(generate-css / verify-render 的运行时依赖),装出的技能无法确定性施工,请检查发布包完整性`
272
+ )
273
+ );
274
+ }
275
+
276
+ // mapping/ 三列变量映射表(区域名 ↔ 项目现状对照)
270
277
  const mappingSrc = path.join(SKILL_DIR, "mapping");
271
278
  if (fs.existsSync(mappingSrc)) {
272
279
  copyDir(mappingSrc, path.join(dest, "mapping"));
273
280
  }
281
+ }
282
+
283
+ /**
284
+ * rules 类型:Windsurf / Trae / CodeBuddy
285
+ * 规则文件承载文本规范;施工脚本与 adapters/mapping 以技能本体随附落盘,
286
+ * 规则文件头部注明本体路径,模型按该路径运行脚本。
287
+ */
288
+ function installRules(targetKey, projectDir) {
289
+ const cfg = TARGETS[targetKey];
290
+ const targetDir = cfg.dir(projectDir);
291
+ ensureDir(targetDir);
292
+
293
+ // 技能本体:<rules 上一级>/skills/<名>/(与 skills 形态同构)
294
+ const bodyDest = path.join(path.dirname(targetDir), "skills", SKILL_NAME);
295
+ copySkillBody(bodyDest);
296
+
297
+ const content = readFullSkill();
298
+ const file = path.join(targetDir, `${SKILL_NAME}.md`);
299
+ const header =
300
+ `> 技能本体(scripts/adapters/mapping/references)已随附安装至: ${bodyDest}\n` +
301
+ `> 运行施工脚本时一律使用该路径,例如:\n` +
302
+ `> node ${bodyDest}/scripts/generate-css.mjs --project <项目根>\n` +
303
+ `> node ${bodyDest}/scripts/check-landing.mjs --project <项目根>\n\n`;
304
+ fs.writeFileSync(file, header + content, "utf-8");
305
+ }
306
+
307
+ /**
308
+ * skills 类型:Cursor / Qoder / Claude / Universal
309
+ */
310
+ function installSkills(targetKey, projectDir) {
311
+ const cfg = TARGETS[targetKey];
312
+ const targetDir = cfg.dir(projectDir);
313
+ const dest = path.join(targetDir, SKILL_NAME);
314
+ copySkillBody(dest);
274
315
 
275
316
  // Cursor:清理旧版 rules/*.mdc,避免与 skills 重复
276
317
  if (cfg.legacyRulesFile) {
@@ -318,6 +359,9 @@ function uninstallTarget(targetKey, projectDir) {
318
359
  const targetDir = cfg.dir(projectDir);
319
360
  const file = path.join(targetDir, `${SKILL_NAME}.md`);
320
361
  if (fs.existsSync(file)) fs.unlinkSync(file);
362
+ // 同步清理随附的技能本体
363
+ const body = path.join(path.dirname(targetDir), "skills", SKILL_NAME);
364
+ if (fs.existsSync(body)) removeDir(body);
321
365
  } else if (cfg.type === "skills") {
322
366
  const targetDir = cfg.dir(projectDir);
323
367
  const dest = path.join(targetDir, SKILL_NAME);
@@ -366,6 +410,7 @@ ${bold("目标 (target):")}
366
410
  windsurf → Windsurf ~/.windsurf/rules/
367
411
  gemini → Gemini CLI <项目>/GEMINI.md(项目级)
368
412
  trae → Trae ~/.trae/rules/
413
+ codebuddy → CodeBuddy ~/.codebuddy/rules/
369
414
  claude → Claude Code ~/.claude/skills/
370
415
  universal → 通用标准 ~/.agents/skills/
371
416
  all → 安装到所有支持的工具
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qilitt-mickey/vue3-temp-skill",
3
- "version": "1.2.5",
3
+ "version": "1.2.6",
4
4
  "type": "module",
5
5
  "description": "Vue 3 企业级中后台项目开发规范技能包 — core-kernel 架构、按需功能模块、视觉契约落地",
6
6
  "bin": {
@@ -12,6 +12,7 @@
12
12
  },
13
13
  "files": [
14
14
  "bin/",
15
+ "adapters/",
15
16
  "references/",
16
17
  "scripts/",
17
18
  "mapping/",
@@ -230,8 +230,14 @@ node <技能>/scripts/generate-css.mjs --project <项目根>
230
230
  1. 在样式入口引入三份文件(**入口位置按 `adapter.output.entryImport`——通常是 main.ts 在组件库全量 CSS 之后**)
231
231
  2. 页面根节点挂 `.ui-page`,各区域挂 `.ui-region-<名>`
232
232
  3. 按 adapter 的「角色类挂载方式」给组件挂 `.role-*`(**表格走插槽,不是在 `<el-table>` 上加类**)
233
- 4. **按契约 `structure` 重建页面骨架**——区块数量/顺序、列数与列宽档位、控件类型、图标 path、折叠/吸附结构,**照骨架对齐**。
234
- **⚠️ 形态/内容分离(红线)**:`structure` 里标了 `sampleOnly` 的业务内容(列名文字、筛选项 label、数据行、菜单文字、状态文案)**一律不烧进项目**——项目侧保持自身业务字段(列名/prop/API/mock/权限/字典),只把「形态」对齐骨架。设计稿的演示占位不是目标项目的业务事实;覆盖业务逻辑 = 交付失败。
233
+ 4. **内容区页面骨架由 `generate-page.mjs` 确定性生成,不手写**:
234
+
235
+ ```bash
236
+ node <技能>/scripts/generate-page.mjs --project <项目根> --out src/views/design-restore
237
+ ```
238
+
239
+ 生成器把契约 `structure` + `pages` 逐条翻译成列表页脚手架 SFC——页头/筛选/工具栏/表格/分页的区块结构与顺序、列数与列宽档位、控件类型、图标、状态变体类、SAMPLE 行。**角色类全部以字面量挂载**(`check-landing.mjs` 用正则扫源文件文本,回调拼接出来的类名扫不到 = 自检必挂)。生成后在路由注册该页。
240
+ **⚠️ 形态/内容分离(红线)**:`structure` 里标了 `sampleOnly` 的业务内容(列名文字、筛选项 label、数据行、状态文案)在生成物里一律是 SAMPLE 占位——项目侧接入真实业务时替换为自身字段(列名/prop/API/mock/权限/字典),**只换内容、不动形态**(角色类、区块结构、列宽档位保持原样)。设计稿的演示占位不是目标项目的业务事实;覆盖业务逻辑 = 交付失败。
235
241
  5. **重跑第 0 步的落地自检,八项全过才算挂载完成**
236
242
  6. 进入第 7 步视觉验收(computed style 逐点比对契约 + visualSanity 通用断言,目视截图只作最后留档)
237
243
 
@@ -19,6 +19,10 @@ import { fileURLToPath } from "node:url";
19
19
  const IGNORE_DIRS = new Set([
20
20
  "node_modules", "dist", ".git", ".vite", "coverage",
21
21
  "__pycache__", ".idea", ".vscode", ".workbuddy", "unpackage",
22
+ // AI 工具配置 / 技能分发目录:技能脚本文本含 CSS 文件名与角色类名,
23
+ // 不排除会被 checkImported / checkMounting 自匹配成假 PASS
24
+ ".agents", ".codebuddy", ".cursor", ".qoder", ".windsurf", ".gemini",
25
+ ".trae", ".claude", ".github",
22
26
  ]);
23
27
 
24
28
  const CSS_FILES = ["ui-tokens.css", "ui-roles.css", "ui-layout.css"];
@@ -222,7 +226,24 @@ function expectedClassName(roleName) {
222
226
  );
223
227
  }
224
228
 
225
- function checkMounting(projectRoot, roleNames) {
229
+ /** 页面绑定引用的角色集合(pages.*.nodes 的 role + pages.*.regions 的 role)—— 挂载检查的「必需集」。
230
+ * 契约角色库可能大于单页实际用量(可选复用角色如 card.base),未被任何页面引用的角色缺失不应阻塞落地。 */
231
+ function boundRoles(contract) {
232
+ const pages = (contract && contract.pages) || {};
233
+ const set = new Set();
234
+ for (const page of Object.values(pages)) {
235
+ if (!page || typeof page !== "object") continue;
236
+ for (const n of Array.isArray(page.nodes) ? page.nodes : []) {
237
+ if (n && n.role) set.add(String(n.role));
238
+ }
239
+ for (const b of Array.isArray(page.regions) ? page.regions : []) {
240
+ if (b && b.role) set.add(String(b.role));
241
+ }
242
+ }
243
+ return set;
244
+ }
245
+
246
+ function checkMounting(projectRoot, roleNames, requiredSet) {
226
247
  const vueFiles = walk(projectRoot, SOURCE_EXT, []);
227
248
  const roleClassRe = /\brole-[a-z0-9]+(?:-[a-z0-9]+)*/g;
228
249
  const usedClasses = new Map();
@@ -239,9 +260,19 @@ function checkMounting(projectRoot, roleNames) {
239
260
  const expected = roleNames.map(expectedClassName);
240
261
  const mounted = [];
241
262
  const missing = [];
263
+ const optionalMissing = [];
264
+ let optionalMounted = 0;
242
265
  for (let i = 0; i < roleNames.length; i++) {
243
- if (usedClasses.has(expected[i])) mounted.push(expected[i]);
244
- else missing.push(`${roleNames[i]} → .${expected[i]}`);
266
+ // 必需集为空时回退旧语义(全部必需)—— 兼容无 pages 绑定的契约
267
+ const required = !requiredSet || requiredSet.size === 0 || requiredSet.has(roleNames[i]);
268
+ if (usedClasses.has(expected[i])) {
269
+ mounted.push(expected[i]);
270
+ if (!required) optionalMounted++;
271
+ } else if (required) {
272
+ missing.push(`${roleNames[i]} → .${expected[i]}`);
273
+ } else {
274
+ optionalMissing.push(`${roleNames[i]} → .${expected[i]}`);
275
+ }
245
276
  }
246
277
 
247
278
  const layoutClassRe = /\bui-(?:page|region-[a-z0-9]+(?:-[a-z0-9]+)*)\b/g;
@@ -256,6 +287,8 @@ function checkMounting(projectRoot, roleNames) {
256
287
  scannedFileCount: vueFiles.length,
257
288
  mounted,
258
289
  missing,
290
+ optionalMissing,
291
+ optionalMounted,
259
292
  layoutClasses: [...layoutClasses],
260
293
  hasLayout: layoutClasses.has("ui-page"),
261
294
  };
@@ -263,14 +296,35 @@ function checkMounting(projectRoot, roleNames) {
263
296
 
264
297
  // ────────────────────────────── 结构校验(契约 §7)
265
298
 
266
- const LIBRARY_DIR = path.resolve(
267
- path.dirname(fileURLToPath(import.meta.url)),
268
- "../../project-ui-design/references/library"
269
- );
299
+ /**
300
+ * 角色库目录多级探测(project-ui-design/references/library):
301
+ * 1. 环境变量 UI_DESIGN_LIBRARY_DIR(显式指定,最高优先)
302
+ * 2. 双包同级(源码仓 skills/ 下、universal/rules 分发后的同级安装目录)
303
+ * 3. 上两层兜底
304
+ * 均不可达时返回 { dir: null, tried } —— 白名单校验降级为 WARN(预期降级,非故障)。
305
+ */
306
+ function resolveLibraryDir() {
307
+ const here = path.dirname(fileURLToPath(import.meta.url));
308
+ const candidates = [];
309
+ if (process.env.UI_DESIGN_LIBRARY_DIR) {
310
+ candidates.push(path.resolve(process.env.UI_DESIGN_LIBRARY_DIR));
311
+ }
312
+ candidates.push(
313
+ path.resolve(here, "../../project-ui-design/references/library"),
314
+ path.resolve(here, "../../../project-ui-design/references/library"),
315
+ );
316
+ for (const dir of candidates) {
317
+ if (fs.existsSync(dir)) return { dir, tried: candidates };
318
+ }
319
+ return { dir: null, tried: candidates };
320
+ }
321
+
322
+ const { dir: LIBRARY_DIR } = resolveLibraryDir();
270
323
 
271
324
  /** 加载角色库名集合:<场景>.json + components.json;目录不可达返回 null(降级 WARN) */
272
325
  function loadRoleLibrary(scene) {
273
326
  const merged = new Set();
327
+ if (!LIBRARY_DIR) return null;
274
328
  for (const f of [`${scene}.json`, "components.json"]) {
275
329
  const p = path.join(LIBRARY_DIR, f);
276
330
  if (!exists(p)) continue;
@@ -435,12 +489,12 @@ function run(projectRoot) {
435
489
  structStatus === "PASS"
436
490
  ? `${parsed.roleNames.length} 个角色全部在角色库;regions 与 areas 一致(${parsed.info.areaCount} 个区域);页面绑定无悬空引用`
437
491
  : structStatus === "WARN"
438
- ? "角色库目录不可达,跳过「角色名在库」校验(regions↔areas 与页面绑定校验已通过)"
492
+ ? "角色库目录不可达,跳过「角色名在库」校验(regions↔areas 与页面绑定校验已通过)—— 分发形态下 project-ui-design 未与本项目技能同装属预期降级"
439
493
  : struct.problems.join(";"),
440
494
  structStatus === "FAIL"
441
495
  ? "设计侧结构违规 —— 回设计侧修正契约(角色名须在库、layout.regions 与 layout.areas 一致、绑定的区域名必须存在),重新出契约。"
442
496
  : structStatus === "WARN"
443
- ? "确认技能包完整(project-ui-design/references/library 存在)后重跑。"
497
+ ? "预期降级,不阻断落地。如需启用角色白名单校验:将 project-ui-design 与 vue3-temp-skill 同级安装,或设置环境变量 UI_DESIGN_LIBRARY_DIR 指向 project-ui-design/references/library 后重跑。"
444
498
  : null
445
499
  );
446
500
  if (structStatus === "FAIL") {
@@ -481,21 +535,26 @@ function run(projectRoot) {
481
535
  );
482
536
  }
483
537
 
484
- // 6. 角色类挂载
485
- const mount = checkMounting(projectRoot, parsed.roleNames);
486
- const mountRatio = parsed.roleNames.length
487
- ? mount.mounted.length / parsed.roleNames.length
488
- : 0;
538
+ // 6. 角色类挂载 —— 必需集 = 页面绑定(nodes/regions)引用的角色;
539
+ // 契约角色库大于单页用量时(可选复用角色),未引用角色的缺失只提示不计入告警
540
+ const requiredRoleSet = boundRoles(contract);
541
+ const mount = checkMounting(projectRoot, parsed.roleNames, requiredRoleSet);
542
+ const optionalCount = mount.optionalMounted + mount.optionalMissing.length;
543
+ const requiredTotal = parsed.roleNames.length - optionalCount;
544
+ const requiredMounted = mount.mounted.length - mount.optionalMounted;
489
545
  add(
490
546
  "roles-mounted",
491
547
  "角色类挂到模板上",
492
- mount.mounted.length === parsed.roleNames.length ? "PASS"
548
+ mount.missing.length === 0 ? "PASS"
493
549
  : mount.mounted.length === 0 ? "FAIL" : "WARN",
494
- `契约 ${parsed.roleNames.length} 个角色,模板挂载 ${mount.mounted.length} 个(覆盖率 ${(mountRatio * 100).toFixed(0)}%);扫描 ${mount.scannedFileCount} 个源文件`,
550
+ `契约 ${parsed.roleNames.length} 个角色(页面绑定引用 ${requiredTotal} 个为必需,已挂载 ${requiredMounted} 个);扫描 ${mount.scannedFileCount} 个源文件` +
551
+ (mount.optionalMissing.length
552
+ ? `;未引用可选角色缺失 ${mount.optionalMissing.length} 个(${mount.optionalMissing.slice(0, 4).map((s) => s.split(" ")[0]).join("、")}${mount.optionalMissing.length > 4 ? "…" : ""}),不影响落地`
553
+ : ""),
495
554
  mount.mounted.length === 0
496
555
  ? "没有任何 .vue 文件出现 role-* 类名 —— 三份 CSS 全是死样式(写了规则但没元素承载)。按 adapter 的挂载映射给组件加 class。"
497
556
  : mount.missing.length
498
- ? `未挂载 ${mount.missing.length} 个角色(CSS 已生成但页面上没有对应 class,视觉不会变): ${mount.missing.slice(0, 6).join(" | ")}${mount.missing.length > 6 ? " …" : ""}`
557
+ ? `必需角色未挂载 ${mount.missing.length} 个(CSS 已生成但页面上没有对应 class,视觉不会变): ${mount.missing.slice(0, 6).join(" | ")}${mount.missing.length > 6 ? " …" : ""}`
499
558
  : null
500
559
  );
501
560