@qilitt-mickey/vue3-temp-skill 1.2.9 → 1.2.10

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/README.md CHANGED
@@ -56,22 +56,44 @@
56
56
  | `references/adapters/vue-element-plus.md` | 实际组件 DOM 与样式落点检查 |
57
57
  | `scripts/modules.json`、`scripts/match.py` | 常规需求的按需规范路由 |
58
58
 
59
- ## 安装与测试
59
+ ## 安装与更新
60
60
 
61
- 要求 Node.js >= 22.12.0;能力路由需要 Python 3,无 Python 时按技能入口表加载规范。本轮改动尚未发布到 npm,先从本源码目录安装到选定编辑器:
61
+ 要求 Node.js >= 22.12.0;能力路由需要 Python 3,无 Python 时按技能入口表加载规范。命令形状与 `project-ui-design` 完全一致,只有包名不同;`-y` 让 npx 跳过确认提示。
62
+
63
+ ```bash
64
+ npx -y @qilitt-mickey/vue3-temp-skill@latest install # 安装:自动探测本机已装编辑器
65
+ npx -y @qilitt-mickey/vue3-temp-skill@latest update # 更新:把已安装目标刷到当前版本
66
+ npx -y @qilitt-mickey/vue3-temp-skill@latest list # 查看装到了哪些目录、哪个版本
67
+ npx -y @qilitt-mickey/vue3-temp-skill@latest uninstall # 卸载:只清理本包托管的文件
68
+ ```
69
+
70
+ 需要精确控制落点时:
71
+
72
+ ```bash
73
+ npx -y @qilitt-mickey/vue3-temp-skill@latest install qoder # 只装 Qoder
74
+ npx -y @qilitt-mickey/vue3-temp-skill@latest install all # 全装,含未探测到的编辑器
75
+ npx -y @qilitt-mickey/vue3-temp-skill@latest install copilot --project /path/to/app # 项目级(copilot/gemini 必须带 --project)
76
+ ```
77
+
78
+ 省略 `target` 是自动探测,`install all` 是显式全装,两者不同;`update` / `uninstall` 不带 `target` 时按已安装记录整批处理。目标与选项以 `--help` 为准。
79
+
80
+ 安装器携带运行时资源;项目级说明使用独立托管区块,避免覆盖用户说明及另一套技能。旧版本冲突按提示处理,勿直接覆盖未知文件。
81
+
82
+ 设计还原需要两套技能同时在位:
83
+
84
+ ```bash
85
+ npx -y @qilitt-mickey/project-ui-design@latest install
86
+ npx -y @qilitt-mickey/vue3-temp-skill@latest install
87
+ ```
88
+
89
+ 在源码仓库内改动后想先自测,用同一套参数直接跑本地 CLI:`node bin/cli.js install qoder`。
62
90
 
63
91
  ```bash
64
- node bin/cli.js --help
65
- node bin/cli.js install qoder
66
- node bin/cli.js install copilot --project /path/to/project
67
- node bin/cli.js list
68
92
  npm test
69
93
  python scripts/match.py --batch
70
94
  ```
71
95
 
72
- 安装器携带运行时资源;项目级说明使用独立托管区块,避免覆盖用户说明及另一套技能。安装目标和参数以 `--help` 为准。旧版本冲突按提示处理,勿直接覆盖未知文件。
73
-
74
- `npx @qilitt-mickey/vue3-temp-skill@latest` 仍指向已发布版本;新版发布前不会包含本轮修改。源码检查通过不代表所有编辑器已经实机验证。
96
+ 源码检查通过不代表所有编辑器已经实机验证。
75
97
 
76
98
  ## 与模板及设计侧的边界
77
99
 
package/SKILL.md CHANGED
@@ -11,7 +11,7 @@ description: 在 vue3-web-temp 及其派生 Vue 3 项目开发功能、修复代
11
11
 
12
12
  - 使用用户指定的项目根或当前已打开的目标项目;读取项目协同说明、`package.json`、锁文件及 Git 状态,保留已有未提交改动。
13
13
  - 读取 `references/core-kernel.md`;以当前源码核对组件、依赖、路由、接口、主题和布局,不把模板清单当成当前项目事实。
14
- - 用户要求设计还原、UI 改版、按图落地,或同轮调用 `project-ui-design` 时,直接进入 `references/design-apply.md`;有原稿但无 JSON 时在同轮调用或读取设计技能,完成提取后继续实施。
14
+ - 用户要求设计还原、UI 改版、按图落地,或同轮调用 `project-ui-design` 时,直接进入 `references/design-apply.md`;有原稿但无 JSON 时在同轮调用或读取设计技能,完成提取后继续实施。无原稿时同样交给设计技能做来源发现与候选选择,不在项目侧索取原稿或自行挑示例。
15
15
  - 只有业务功能需求时运行 `python <本技能>/scripts/match.py "<需求>" --json`,按 `load_files` 加载专项规范;无 Python 时按下表选取。
16
16
  - 同时有业务增量和视觉需求时,分别列出用户授权的业务变更与视觉变更;Demo 不提供新增业务需求。
17
17
  - 完成条件:项目根、目标范围、任务类型和实际可用规范已确定;不在技能仓库中写目标业务代码。
package/bin/cli.js CHANGED
@@ -4,9 +4,12 @@ import fs from "node:fs";
4
4
  import path from "node:path";
5
5
  import os from "node:os";
6
6
  import { fileURLToPath } from "node:url";
7
+ import { createUi, DEFAULT_SINK } from "./ui.mjs";
7
8
 
8
9
  const PACKAGE_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
9
10
  export const SKILL_NAME = "vue3-temp-skill";
11
+ // 大字 logo 文案与市面 skills CLI 完全一致(ANSI Shadow 的 SKILLS,43 列,80 列终端不会折行);品牌名走副标题,避免长包名把字块撑破。
12
+ export const LOGO_TEXT = "SKILLS";
10
13
  export const RUNTIME_DIRS = ["references", "scripts", "adapters", "mapping"];
11
14
  // 保留已有改动:generate-page.mjs 不再分发;作者侧 cli、测试及临时产物不属于运行资源。
12
15
  export const RUNTIME_SCRIPT_FILES = ["match.py", "modules.json", "check-landing.mjs", "generate-css.mjs", "verify-render.mjs"];
@@ -14,14 +17,20 @@ const RUNTIME_FILES = ["SKILL.md", "README.md"];
14
17
  const MANIFEST = ".skill-install.json";
15
18
  const BEGIN = `<!-- begin ${SKILL_NAME} -->`;
16
19
  const END = `<!-- end ${SKILL_NAME} -->`;
17
- const AUTO_DETECT = false;
20
+ const AUTO_DETECT = true;
18
21
 
19
22
  // 可注入 HOME、包根和 cwd;import 本身不会安装、写记录或退出进程。
20
- export function createInstaller({ home = os.homedir(), skillDir = PACKAGE_DIR, cwd = process.cwd(), log = console.log } = {}) {
23
+ export function createInstaller({ home = os.homedir(), skillDir = PACKAGE_DIR, cwd = process.cwd(), log: sink, ui } = {}) {
21
24
  home = path.resolve(home);
22
25
  skillDir = path.resolve(skillDir);
23
26
  cwd = path.resolve(cwd);
24
- const version = JSON.parse(fs.readFileSync(path.join(skillDir, "package.json"), "utf8")).version;
27
+ // 注入 log 视为脚本消费:纯文本、无颜色、无横幅,保证测试与管道输出稳定。
28
+ const interactive = sink === undefined && Boolean(process.stdout?.isTTY) && Boolean(process.stdin?.isTTY);
29
+ const display = ui ?? createUi({ write: sink ?? DEFAULT_SINK, tty: interactive });
30
+ const log = (line) => display.write(line);
31
+ const pkg = JSON.parse(fs.readFileSync(path.join(skillDir, "package.json"), "utf8"));
32
+ const version = pkg.version;
33
+ const packageName = pkg.name || SKILL_NAME;
25
34
  const trackFile = path.join(home, `.${SKILL_NAME}.json`);
26
35
  // 编辑器专属落点只属于安装器,不进入运行技能的协议或编排。
27
36
  const targets = {
@@ -300,12 +309,49 @@ export function createInstaller({ home = os.homedir(), skillDir = PACKAGE_DIR, c
300
309
  return opts;
301
310
  }
302
311
  function help() {
303
- log(`${SKILL_NAME} v${version}\n用法: ${SKILL_NAME} <install|update|uninstall|list> [target|all] [--project <dir>] [--force]\n目标: ${Object.keys(targets).join(", ")}\nCopilot/Gemini 必须指定 --project;批量全局安装跳过项目专属目标。\nQoder 全局目录: ~/.qoder/skills\n--force 仅迁移可识别的自有旧资源,不能覆盖未管理文件。旧 Qoder / Cursor rules 不会被自动删除。`);
312
+ // 大字 logo 只在交互式终端出现;管道与脚本消费只读文本,避免把日志刷成噪声。
313
+ if (interactive) display.logo(LOGO_TEXT);
314
+ display.write(display.fit(` ${display.bold(SKILL_NAME)} ${display.dim(`v${version}`)} ${display.dim("·")} ${display.dim(pkg.description || "AI 技能安装器")}`));
315
+ display.write("");
316
+ display.write(` ${display.bold("用法")}`);
317
+ display.write(` ${display.cyan(`npx ${packageName}@latest`)} ${display.dim("<install|update|uninstall|list>")} [target|all] [--project <dir>] [--force]`);
318
+ display.write("");
319
+ display.write(` ${display.bold("命令")}`);
320
+ display.write(` ${display.white("install")} [target|all] ${display.dim("省略 target 时自动探测本机已装的编辑器")}`);
321
+ display.write(` ${display.white("update")} [target|all] ${display.dim("把已安装目标更新到当前版本")}`);
322
+ display.write(` ${display.white("list")} ${display.dim("查看已安装记录与所在目录")}`);
323
+ display.write(` ${display.white("uninstall")} [target|all] ${display.dim("卸载,只清理本包托管的文件")}`);
324
+ display.write("");
325
+ display.write(` ${display.bold("目标")}`);
326
+ display.write(` ${display.white(Object.keys(targets).filter((key) => !targets[key].projectOnly).join(", "))} ${display.dim("全局安装")}`);
327
+ display.write(` ${display.white(Object.keys(targets).filter((key) => targets[key].projectOnly).join(", "))} ${display.dim("仅项目安装,必须带 --project")}`);
328
+ display.write("");
329
+ display.write(` ${display.bold("选项")}`);
330
+ display.write(` ${display.white("--project")} <dir> ${display.dim("安装到指定项目根目录")}`);
331
+ display.write(` ${display.white("--force")} ${display.dim("仅迁移可识别的自有旧资源,不覆盖未管理文件")}`);
332
+ display.write(` ${display.white("-v, --version")} ${display.dim("输出版本号")}`);
333
+ display.write(` ${display.white("-h, --help")} ${display.dim("输出本帮助")}`);
334
+ display.write("");
335
+ display.write(` ${display.dim(`Qoder 全局目录: ${shortPath(path.join(home, targets.qoder.dir, SKILL_NAME))}`)}`);
336
+ display.write(` ${display.dim("旧的 Qoder / Cursor rules 文件不会被自动删除,需要时手工清理。")}`);
337
+ display.write("");
338
+ }
339
+ function banner() {
340
+ if (!interactive) return;
341
+ display.logo(LOGO_TEXT);
342
+ display.write(display.fit(` ${display.bold(SKILL_NAME)} ${display.dim(`v${version}`)} ${display.dim("·")} ${display.dim(pkg.description || "AI 技能安装器")}`));
343
+ display.write("");
344
+ display.usage([
345
+ [`npx ${packageName}@latest install`, "自动探测编辑器并安装"],
346
+ [`npx ${packageName}@latest update`, "更新到当前版本"],
347
+ [`npx ${packageName}@latest --help`, "查看全部目标与参数"],
348
+ ]);
349
+ display.write("");
304
350
  }
305
351
  function run(argv) {
306
352
  const opts = parseArgs(argv);
307
353
  if (opts.help || (!opts.positional.length && !opts.version)) { help(); return; }
308
- if (opts.version) { log(version); return; }
354
+ if (opts.version) { log(version); return { done: 0, failed: 0 }; }
309
355
  let [command, target] = opts.positional;
310
356
  if (targets[command] || command === "all" || (AUTO_DETECT && command === "系统")) {
311
357
  if (target) throw new Error("多余的位置参数");
@@ -317,8 +363,15 @@ export function createInstaller({ home = os.homedir(), skillDir = PACKAGE_DIR, c
317
363
  if (target && target !== "all" && !targets[target]) throw new Error(`未知目标: ${target}`);
318
364
  if (command === "list") {
319
365
  if (target || opts.project || opts.force) throw new Error("list 不接受目标或安装参数");
320
- for (const entry of readTrack()) log(`${entry.target} v${entry.version} ${entry.project || home}`);
321
- return;
366
+ const entries = readTrack();
367
+ if (!entries.length) { log(`未找到 ${SKILL_NAME} 的已安装记录。执行 npx ${packageName}@latest install 安装。`); return { done: 0, failed: 0 }; }
368
+ display.note(`${SKILL_NAME} v${version} · 已安装 ${entries.length} 个目标`, entries.map((entry) => {
369
+ const behind = entry.version && entry.version !== version;
370
+ return `${entry.target.padEnd(11)}${display.dim(entry.path || entry.project || home)}${behind ? ` ${display.yellow(`v${entry.version} → v${version}`)}` : ""}`;
371
+ }));
372
+ display.write(display.dim(` 更新全部目标: npx ${packageName}@latest update`));
373
+ display.write("");
374
+ return { done: entries.length, failed: 0 };
322
375
  }
323
376
  let track = readTrack();
324
377
  let jobs;
@@ -327,9 +380,14 @@ export function createInstaller({ home = os.homedir(), skillDir = PACKAGE_DIR, c
327
380
  const keys = target && target !== "all" ? [target] : Object.keys(targets).filter((key) => {
328
381
  const cfg = targets[key];
329
382
  if (cfg.projectOnly) return Boolean(opts.project);
330
- return !AUTO_DETECT || Boolean(stat(path.join(opts.project || home, cfg.dir)));
383
+ // all 是显式全装;只有省略 target 时才按本机已装编辑器自动探测。
384
+ if (target === "all" || !AUTO_DETECT) return true;
385
+ return Boolean(stat(path.join(opts.project || home, cfg.dir)));
331
386
  });
332
387
  jobs = keys.map((key) => ({ target: key, project: opts.project || null }));
388
+ if (!jobs.length) {
389
+ throw new Error(`未检测到已安装的编辑器。可指定目标:npx ${packageName}@latest install <${Object.keys(targets).filter((key) => !targets[key].projectOnly).join("|")}>,或全装:install all。`);
390
+ }
333
391
  } else {
334
392
  jobs = track.filter((entry) => !target || target === "all" || entry.target === target);
335
393
  if (opts.project !== undefined) jobs = jobs.map((entry) => ({ ...entry, project: opts.project }));
@@ -343,24 +401,84 @@ export function createInstaller({ home = os.homedir(), skillDir = PACKAGE_DIR, c
343
401
  }
344
402
  jobs = batch;
345
403
  }
404
+ if (!jobs.length) {
405
+ if (command === "update") throw new Error(`未找到 ${SKILL_NAME} 的已安装记录,无法更新。请先执行 npx ${packageName}@latest install。`);
406
+ log(`未找到 ${SKILL_NAME} 的已安装记录,无需处理。`);
407
+ return { done: 0, failed: 0 };
408
+ }
346
409
  }
347
410
  const identity = (entry) => JSON.stringify([entry.target, entry.project]);
348
411
  jobs = [...new Map(jobs.map((entry) => [identity(entry), entry])).values()];
412
+ // 显式指定 target 时保持严格;批量时任一目标失败不中止整批。
413
+ const batch = !target || target === "all";
349
414
  // 参数与所有项目根在写入前校验;显式 --project 始终覆盖旧记录的 project。
350
- for (const job of jobs) paths(job.target, job.project);
351
- if (!jobs.length) { log("未找到需要处理的目标。"); return; }
415
+ const live = [];
416
+ const stale = [];
352
417
  for (const job of jobs) {
353
- const dest = command === "uninstall" ? uninstall(job.target, job.project, opts.force) : install(job.target, job.project, opts.force);
418
+ try { paths(job.target, job.project); live.push(job); }
419
+ catch (err) {
420
+ // 项目根已被删除的旧记录会永久卡住 update/uninstall,这里单独摘出来并清理。
421
+ if (!batch || !job.project || stat(job.project)?.isDirectory()) throw err;
422
+ stale.push(job);
423
+ }
424
+ }
425
+ jobs = live;
426
+ banner();
427
+ for (const job of stale) {
354
428
  track = track.filter((entry) => identity(entry) !== identity(job));
355
- if (command !== "uninstall") track.push({ target: job.target, project: job.project, version, path: dest });
356
- writeTrack(track);
357
- log(`${command} ${job.target} → ${dest}`);
429
+ display.warn(`${job.target} 的项目目录已不存在,跳过并清理该记录:${display.dim(job.project)}`);
430
+ }
431
+ if (stale.length) writeTrack(track);
432
+ if (!jobs.length) {
433
+ if (!stale.length) log("未找到需要处理的目标。");
434
+ return { done: 0, failed: 0 };
358
435
  }
436
+ const recorded = new Map(track.map((entry) => [identity(entry), entry.version]));
437
+ const verb = { install: "安装", update: "更新", uninstall: "卸载" }[command];
438
+ let done = 0;
439
+ let failed = 0;
440
+ for (const job of jobs) {
441
+ const key = identity(job);
442
+ const from = recorded.get(key);
443
+ try {
444
+ const dest = command === "uninstall" ? uninstall(job.target, job.project, opts.force) : install(job.target, job.project, opts.force);
445
+ track = track.filter((entry) => identity(entry) !== key);
446
+ if (command !== "uninstall") track.push({ target: job.target, project: job.project, version, path: dest });
447
+ writeTrack(track);
448
+ done++;
449
+ const note = command === "uninstall" ? display.dim("已移除")
450
+ : from && from !== version ? display.green(`v${from} → v${version}`)
451
+ : from === version ? display.dim(`v${version} 内容已刷新`) : display.dim(`v${version}`);
452
+ display.ok(`${job.target} ${note}`);
453
+ display.arrow(display.dim(shortPath(dest)));
454
+ } catch (err) {
455
+ if (!batch) throw err;
456
+ failed++;
457
+ display.fail(`${job.target} ${display.dim(String(err.message).split("\n")[0])}`);
458
+ }
459
+ }
460
+ display.note(`${verb}完成 ${done} 个目标 · ${SKILL_NAME} v${version}${failed ? ` · ${display.red(`${failed} 个失败`)}` : ""}`,
461
+ [command === "uninstall" ? `重装:npx ${packageName}@latest install` : `在编辑器里输入 /${SKILL_NAME} 使用;未生效时重启编辑器或新开会话。`]);
462
+ return { done, failed };
463
+ }
464
+ /** 安装行只显示相对形式,完整路径由 list 给出,避免长绝对路径淹没结果。 */
465
+ function shortPath(file) {
466
+ const absolute = path.resolve(file);
467
+ if (absolute === home || absolute.startsWith(home + path.sep)) return `~/${path.relative(home, absolute).split(path.sep).join("/")}`;
468
+ const relative = path.relative(cwd, absolute);
469
+ return relative && !relative.startsWith("..") && !path.isAbsolute(relative) ? `./${relative.split(path.sep).join("/")}` : absolute.split(path.sep).join("/");
359
470
  }
360
471
  return { run, paths, runtimeFiles };
361
472
  }
362
473
 
363
474
  if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
364
- try { createInstaller().run(process.argv.slice(2)); }
365
- catch (err) { console.error(err.message); process.exitCode = 1; }
475
+ try {
476
+ const result = createInstaller().run(process.argv.slice(2));
477
+ if (result?.failed) process.exitCode = 1;
478
+ }
479
+ catch (err) {
480
+ const mark = process.stdout.isTTY && !process.env.NO_COLOR && process.env.TERM !== "dumb" ? "\x1b[31m✗\x1b[0m" : "!";
481
+ console.error(`${mark} ${err.message}`);
482
+ process.exitCode = 1;
483
+ }
366
484
  }
package/bin/ui.mjs ADDED
@@ -0,0 +1,152 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * 安装器终端展示层(零依赖;两个技能包共用同一份实现)。
5
+ *
6
+ * 视觉规格对齐 npm 包 skills(skills.sh):ANSI Shadow 大字 logo + 6 级灰度渐变 + dim 副标题,
7
+ * 结果行使用 ✓ / → / ! / ✗ 前缀,收尾使用汇总块。
8
+ * 非 TTY、NO_COLOR、TERM=dumb、CI,或调用方注入自定义 log 时自动降级为纯文本,
9
+ * 保证脚本消费与测试断言稳定。
10
+ */
11
+
12
+ const GLYPHS = {
13
+ "0": [" ██████╗ ","██╔═████╗","██║██╔██║","████╔╝██║","╚██████╔╝"," ╚═════╝ "],
14
+ "1": [" ██╗","███║","╚██║"," ██║"," ██║"," ╚═╝"],
15
+ "2": ["██████╗ ","╚════██╗"," █████╔╝","██╔═══╝ ","███████╗","╚══════╝"],
16
+ "3": ["██████╗ ","╚════██╗"," █████╔╝"," ╚═══██╗","██████╔╝","╚═════╝ "],
17
+ "4": ["██╗ ██╗","██║ ██║","███████║","╚════██║"," ██║"," ╚═╝"],
18
+ "5": ["███████╗","██╔════╝","███████╗","╚════██║","███████║","╚══════╝"],
19
+ "6": [" ██████╗ ","██╔════╝ ","███████╗ ","██╔═══██╗","╚██████╔╝"," ╚═════╝ "],
20
+ "7": ["███████╗","╚════██║"," ██╔╝"," ██╔╝ "," ██║ "," ╚═╝ "],
21
+ "8": [" █████╗ ","██╔══██╗","╚█████╔╝","██╔══██╗","╚█████╔╝"," ╚════╝ "],
22
+ "9": [" █████╗ ","██╔══██╗","╚██████║"," ╚═══██║"," █████╔╝"," ╚════╝ "],
23
+ "A": [" █████╗ ","██╔══██╗","███████║","██╔══██║","██║ ██║","╚═╝ ╚═╝"],
24
+ "B": ["██████╗ ","██╔══██╗","██████╔╝","██╔══██╗","██████╔╝","╚═════╝ "],
25
+ "C": [" ██████╗","██╔════╝","██║ ","██║ ","╚██████╗"," ╚═════╝"],
26
+ "D": ["██████╗ ","██╔══██╗","██║ ██║","██║ ██║","██████╔╝","╚═════╝ "],
27
+ "E": ["███████╗","██╔════╝","█████╗ ","██╔══╝ ","███████╗","╚══════╝"],
28
+ "F": ["███████╗","██╔════╝","█████╗ ","██╔══╝ ","██║ ","╚═╝ "],
29
+ "G": [" ██████╗ ","██╔════╝ ","██║ ███╗","██║ ██║","╚██████╔╝"," ╚═════╝ "],
30
+ "H": ["██╗ ██╗","██║ ██║","███████║","██╔══██║","██║ ██║","╚═╝ ╚═╝"],
31
+ "I": ["██╗","██║","██║","██║","██║","╚═╝"],
32
+ "J": [" ██╗"," ██║"," ██║","██ ██║","╚█████╔╝"," ╚════╝ "],
33
+ "K": ["██╗ ██╗","██║ ██╔╝","█████╔╝ ","██╔═██╗ ","██║ ██╗","╚═╝ ╚═╝"],
34
+ "L": ["██╗ ","██║ ","██║ ","██║ ","███████╗","╚══════╝"],
35
+ "M": ["███╗ ███╗","████╗ ████║","██╔████╔██║","██║╚██╔╝██║","██║ ╚═╝ ██║","╚═╝ ╚═╝"],
36
+ "N": ["███╗ ██╗","████╗ ██║","██╔██╗ ██║","██║╚██╗██║","██║ ╚████║","╚═╝ ╚═══╝"],
37
+ "O": [" ██████╗ ","██╔═══██╗","██║ ██║","██║ ██║","╚██████╔╝"," ╚═════╝ "],
38
+ "P": ["██████╗ ","██╔══██╗","██████╔╝","██╔═══╝ ","██║ ","╚═╝ "],
39
+ "Q": [" ██████╗ ","██╔═══██╗","██║ ██║","██║▄▄ ██║","╚██████╔╝"," ╚══▀▀═╝ "],
40
+ "R": ["██████╗ ","██╔══██╗","██████╔╝","██╔══██╗","██║ ██║","╚═╝ ╚═╝"],
41
+ "S": ["███████╗","██╔════╝","███████╗","╚════██║","███████║","╚══════╝"],
42
+ "T": ["████████╗","╚══██╔══╝"," ██║ "," ██║ "," ██║ "," ╚═╝ "],
43
+ "U": ["██╗ ██╗","██║ ██║","██║ ██║","██║ ██║","╚██████╔╝"," ╚═════╝ "],
44
+ "V": ["██╗ ██╗","██║ ██║","██║ ██║","╚██╗ ██╔╝"," ╚████╔╝ "," ╚═══╝ "],
45
+ "W": ["██╗ ██╗","██║ ██║","██║ █╗ ██║","██║███╗██║","╚███╔███╔╝"," ╚══╝╚══╝ "],
46
+ "X": ["██╗ ██╗","╚██╗██╔╝"," ╚███╔╝ "," ██╔██╗ ","██╔╝ ██╗","╚═╝ ╚═╝"],
47
+ "Y": ["██╗ ██╗","╚██╗ ██╔╝"," ╚████╔╝ "," ╚██╔╝ "," ██║ "," ╚═╝ "],
48
+ "Z": ["███████╗","╚══███╔╝"," ███╔╝ "," ███╔╝ ","███████╗","╚══════╝"],
49
+ "-": [" "," ","█████╗","╚════╝"," "," "],
50
+ };
51
+
52
+ const LOGO_ROWS = 6;
53
+ // 与 skills CLI 相同的由亮到暗渐变,让大字有层次而不是纯色块。
54
+ const LOGO_GRAYS = ["38;5;250", "38;5;248", "38;5;245", "38;5;243", "38;5;240", "38;5;238"];
55
+
56
+ export function logoWidth(text) {
57
+ return [...String(text).toUpperCase()].reduce((sum, char) => sum + (GLYPHS[char]?.[0].length ?? 0), 0);
58
+ }
59
+
60
+ /** 逐字拼接 ANSI Shadow 字形;与 figlet 整词渲染逐字节一致。 */
61
+ export function renderLogo(text) {
62
+ const cells = [...String(text).toUpperCase()].map((char) => GLYPHS[char] ?? GLYPHS["-"]);
63
+ return Array.from({ length: LOGO_ROWS }, (_, row) => cells.map((glyph) => glyph[row]).join("").replace(/\s+$/, ""));
64
+ }
65
+
66
+ export const DEFAULT_SINK = (line) => process.stdout.write(`${line}\n`);
67
+
68
+ /** 东亚宽字符按 2 列计算,避免中英混排时用法行错位。 */
69
+ export function displayWidth(text) {
70
+ let width = 0;
71
+ for (const char of String(text)) {
72
+ const code = char.codePointAt(0);
73
+ const wide = (code >= 0x1100 && code <= 0x115f) || (code >= 0x2e80 && code <= 0x303e) || (code >= 0x3041 && code <= 0x33ff) ||
74
+ (code >= 0x4e00 && code <= 0x9fff) || (code >= 0xa000 && code <= 0xa4cf) || (code >= 0xac00 && code <= 0xd7a3) ||
75
+ (code >= 0xf900 && code <= 0xfaff) || (code >= 0xfe30 && code <= 0xfe6f) || (code >= 0xff00 && code <= 0xff60) ||
76
+ (code >= 0xffe0 && code <= 0xffe6) || (code >= 0x20000 && code <= 0x2fffd);
77
+ width += wide ? 2 : 1;
78
+ }
79
+ return width;
80
+ }
81
+
82
+ export function createUi({ write = DEFAULT_SINK, tty = Boolean(process.stdout?.isTTY), env = process.env, columns = process.stdout?.columns || 80 } = {}) {
83
+ const color = Boolean(tty) && !env.NO_COLOR && env.TERM !== "dumb";
84
+ const strip = (line) => String(line).replace(/\x1b\[[0-9;]*m/g, "");
85
+ const paint = (code) => (text) => (color ? `\x1b[${code}m${text}\x1b[0m` : String(text));
86
+ const bold = paint(1);
87
+ const dim = paint(2);
88
+ const red = paint(31);
89
+ const green = paint(32);
90
+ const yellow = paint(33);
91
+ const cyan = paint(36);
92
+ const white = paint(39);
93
+ return {
94
+ color,
95
+ write,
96
+ columns,
97
+ bold,
98
+ dim,
99
+ red,
100
+ green,
101
+ yellow,
102
+ cyan,
103
+ white,
104
+ /** 大字 logo;终端宽度不足时退化为普通标题行,避免块状字被硬换行打散。 */
105
+ logo(text) {
106
+ write("");
107
+ if (logoWidth(text) > Math.max(40, columns - 2)) write(bold(white(text.toUpperCase())));
108
+ else renderLogo(text).forEach((line, row) => write(color ? `\x1b[${LOGO_GRAYS[row]}m${line}\x1b[0m` : line));
109
+ write("");
110
+ },
111
+ /** 用法行:命令取 cyan,说明取 dim,按显示宽度对齐;放不下说明时只保留命令。 */
112
+ usage(entries) {
113
+ const width = Math.max(...entries.map(([command]) => displayWidth(command)));
114
+ for (const [command, hint] of entries) {
115
+ const pad = " ".repeat(width - displayWidth(command));
116
+ const room = hint && columns - (width + pad.length + 8) >= displayWidth(hint);
117
+ write(` ${dim("$")} ${cyan(command)}${pad}${room ? ` ${dim(hint)}` : ""}`);
118
+ }
119
+ },
120
+ step(message) { write(`${dim("·")} ${message}`); },
121
+ ok(message) { write(`${green("✓")} ${message}`); },
122
+ warn(message) { write(`${yellow("!")} ${message}`); },
123
+ fail(message) { write(`${red("✗")} ${message}`); },
124
+ /** 按终端显示宽度截断,保留颜色转义序列;市面 CLI 的横幅副标题同样是一行不折行的短句。 */
125
+ fit(line, width = columns - 1) {
126
+ if (displayWidth(strip(line)) <= width) return line;
127
+ const limit = width - 1;
128
+ let out = "";
129
+ let used = 0;
130
+ let cut = false;
131
+ for (const part of String(line).split(/(\x1b\[[0-9;]*m)/)) {
132
+ if (part.startsWith("\x1b[")) { out += part; continue; }
133
+ if (cut) continue;
134
+ for (const char of part) {
135
+ const size = displayWidth(char);
136
+ if (used + size > limit) { cut = true; break; }
137
+ out += char;
138
+ used += size;
139
+ }
140
+ }
141
+ return `${out.replace(/\x1b\[0m$/, "").replace(/\s+$/, "")}…${color ? "\x1b[0m" : ""}`;
142
+ },
143
+ arrow(message) { write(` ${dim("→")} ${this.fit(message, columns - 4)}`); },
144
+ /** 汇总块:一个加粗标题 + 缩进明细,等价于 skills CLI 的 note()。 */
145
+ note(title, lines = []) {
146
+ write("");
147
+ write(this.fit(bold(title)));
148
+ for (const line of lines) write(this.fit(` ${line}`, columns - 1));
149
+ write("");
150
+ },
151
+ };
152
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qilitt-mickey/vue3-temp-skill",
3
- "version": "1.2.9",
3
+ "version": "1.2.10",
4
4
  "type": "module",
5
5
  "description": "vue3-web-temp 项目工程技能:按需功能开发、保留业务的设计还原与视觉及功能双重验证",
6
6
  "bin": {
@@ -3,6 +3,7 @@
3
3
  ## 1. 接入设计与确定范围
4
4
 
5
5
  - 接受原始截图、HTML、设计工程、运行地址、`ui.design-handoff/1.0` 或已有 `ui.visual-contract/1.0`;原稿已有而交接缺失时,在本轮加载 `project-ui-design` 补齐交接并继续,不要求用户切会话。
6
+ - 项目内没有任何设计来源时,不在项目侧替用户决定用哪份示例:转由 `project-ui-design` 按其 `references/scope-gate.md` 走完来源发现,就「系统场景 / 页面类型 / 主题」一次提问,选定后自动定位内置 `mockup` 与 `preview` 并回到本节继续实施。项目侧不重复实现来源选择逻辑,也不因缺少附件而停在索取原稿。
6
7
  - 读取当前任务的交接和原稿;多个任务并存时按用户指定或上下文选择,无法确定时询问,不默认选目录中的第一份 JSON。
7
8
  - 从路由与页面确定目标。用户指定页面时使用该页面;用户要求全系统时覆盖共用外壳及受影响页面类型,列出代表性页面和独有视图,不缩成一页。
8
9
  - 原稿有外壳而内容页未确定时,可先做已明确的共用外壳;页面锚点存在歧义时一次询问。没有业务页面时不照 Demo 新建业务;只有用户另行授权业务开发才创建路由和业务页。