@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/SKILL.md +3 -3
- package/adapters/vue-element-plus.json +634 -0
- package/bin/cli.js +72 -27
- package/package.json +2 -1
- package/references/design-apply.md +8 -2
- package/scripts/check-landing.mjs +76 -17
- package/scripts/generate-page.mjs +734 -0
- package/scripts/verify-render.mjs +2 -0
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
|
-
//
|
|
35
|
-
//
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
const RUNTIME_SCRIPT_FILES = [
|
|
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
|
-
*
|
|
226
|
-
|
|
227
|
-
|
|
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
|
|
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
|
-
//
|
|
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.
|
|
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.
|
|
234
|
-
|
|
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
|
-
|
|
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
|
-
|
|
244
|
-
|
|
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
|
-
|
|
267
|
-
|
|
268
|
-
|
|
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
|
-
? "
|
|
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
|
-
|
|
486
|
-
const
|
|
487
|
-
|
|
488
|
-
|
|
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.
|
|
548
|
+
mount.missing.length === 0 ? "PASS"
|
|
493
549
|
: mount.mounted.length === 0 ? "FAIL" : "WARN",
|
|
494
|
-
`契约 ${parsed.roleNames.length}
|
|
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
|
-
?
|
|
557
|
+
? `必需角色未挂载 ${mount.missing.length} 个(CSS 已生成但页面上没有对应 class,视觉不会变): ${mount.missing.slice(0, 6).join(" | ")}${mount.missing.length > 6 ? " …" : ""}`
|
|
499
558
|
: null
|
|
500
559
|
);
|
|
501
560
|
|