@qilitt-mickey/vue3-temp-skill 1.2.5 → 1.2.7
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 +51 -115
- package/SKILL.md +41 -159
- package/adapters/vue-element-plus.json +634 -0
- package/bin/cli.js +297 -612
- package/package.json +6 -2
- package/references/adapters/_new-adapter.md +17 -82
- package/references/adapters/vue-antdv.md +21 -107
- package/references/adapters/vue-element-plus.md +36 -179
- package/references/code-quality.md +52 -168
- package/references/core-kernel.md +12 -3
- package/references/crud-pages.md +10 -11
- package/references/data-screen.md +8 -9
- package/references/design-apply.md +57 -352
- package/references/layout-theme.md +6 -4
- package/references/visual-verification.md +48 -0
- package/scripts/check-landing.mjs +308 -62
- package/scripts/generate-css.mjs +17 -397
- package/scripts/match.py +30 -11
- package/scripts/modules.json +3 -3
- package/scripts/verify-render.mjs +17 -522
package/bin/cli.js
CHANGED
|
@@ -1,659 +1,344 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
/**
|
|
4
|
-
* vue3-temp-skill CLI
|
|
5
|
-
*
|
|
6
|
-
* 将 Vue 3 项目开发规范技能安装到指定 AI 工具
|
|
7
|
-
*
|
|
8
|
-
* 用法:
|
|
9
|
-
* vue3-temp-skill install <target> [options] 安装到指定 AI 工具
|
|
10
|
-
* vue3-temp-skill update [target] [options] 更新已安装的技能(默认更新全部已安装目标)
|
|
11
|
-
* vue3-temp-skill uninstall [target] [options] 卸载已安装的技能(默认卸载全部)
|
|
12
|
-
* vue3-temp-skill list 查看已安装的目标
|
|
13
|
-
* vue3-temp-skill --help 帮助
|
|
14
|
-
* vue3-temp-skill --version 版本号
|
|
15
|
-
*
|
|
16
|
-
* 通过 npx / pnpx 使用时自动下载最新版本:
|
|
17
|
-
* npx @qilitt-mickey/vue3-temp-skill@latest install cursor
|
|
18
|
-
* npx @qilitt-mickey/vue3-temp-skill@latest update
|
|
19
|
-
*/
|
|
20
|
-
|
|
21
3
|
import fs from "node:fs";
|
|
22
4
|
import path from "node:path";
|
|
23
5
|
import os from "node:os";
|
|
24
|
-
import { execFileSync, execSync } from "node:child_process";
|
|
25
6
|
import { fileURLToPath } from "node:url";
|
|
26
7
|
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
const
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
type: "
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
name: "
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
name: "Windsurf",
|
|
68
|
-
dir: (p) => (p ? path.join(p, ".windsurf/rules") : path.join(HOME, ".windsurf/rules")),
|
|
69
|
-
type: "rules",
|
|
70
|
-
},
|
|
71
|
-
gemini: {
|
|
72
|
-
name: "Gemini CLI",
|
|
73
|
-
// 仅项目级:必须指定 --project,否则相对当前工作目录
|
|
74
|
-
file: (p) => path.join(p || process.cwd(), "GEMINI.md"),
|
|
75
|
-
type: "merge",
|
|
76
|
-
projectOnly: true,
|
|
77
|
-
},
|
|
78
|
-
trae: {
|
|
79
|
-
name: "Trae",
|
|
80
|
-
dir: (p) => (p ? path.join(p, ".trae/rules") : path.join(HOME, ".trae/rules")),
|
|
81
|
-
type: "rules",
|
|
82
|
-
},
|
|
83
|
-
claude: {
|
|
84
|
-
name: "Claude Code",
|
|
85
|
-
dir: (p) => (p ? path.join(p, ".claude/skills") : path.join(HOME, ".claude/skills")),
|
|
86
|
-
type: "skills",
|
|
87
|
-
},
|
|
88
|
-
universal: {
|
|
89
|
-
name: "Universal (.agents)",
|
|
90
|
-
// 全局 → ~/.agents/skills;项目 → <project>/.agents/skills
|
|
91
|
-
dir: (p) => (p ? path.join(p, ".agents/skills") : path.join(HOME, ".agents/skills")),
|
|
92
|
-
type: "skills",
|
|
93
|
-
},
|
|
94
|
-
};
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* 解析安装目标的可读路径(用于日志)
|
|
98
|
-
*/
|
|
99
|
-
function resolveTargetPath(targetKey, projectDir) {
|
|
100
|
-
const cfg = TARGETS[targetKey];
|
|
101
|
-
if (cfg.type === "merge") return cfg.file(projectDir);
|
|
102
|
-
if (cfg.type === "rules") return path.join(cfg.dir(projectDir), `${SKILL_NAME}.md`);
|
|
103
|
-
return path.join(cfg.dir(projectDir), SKILL_NAME);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
// ============================================================
|
|
107
|
-
// 样式
|
|
108
|
-
// ============================================================
|
|
109
|
-
const cyan = (s) => `\x1b[36m${s}\x1b[0m`;
|
|
110
|
-
const green = (s) => `\x1b[32m${s}\x1b[0m`;
|
|
111
|
-
const red = (s) => `\x1b[31m${s}\x1b[0m`;
|
|
112
|
-
const yellow = (s) => `\x1b[33m${s}\x1b[0m`;
|
|
113
|
-
const bold = (s) => `\x1b[1m${s}\x1b[0m`;
|
|
114
|
-
const dim = (s) => `\x1b[2m${s}\x1b[0m`;
|
|
115
|
-
|
|
116
|
-
// ============================================================
|
|
117
|
-
// 工具函数
|
|
118
|
-
// ============================================================
|
|
119
|
-
|
|
120
|
-
function ensureDir(dir) {
|
|
121
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
/**
|
|
125
|
-
* 复制单个文件
|
|
126
|
-
*/
|
|
127
|
-
function copyFile(src, dest) {
|
|
128
|
-
if (fs.existsSync(src)) {
|
|
129
|
-
fs.copyFileSync(src, dest);
|
|
8
|
+
const PACKAGE_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
9
|
+
export const SKILL_NAME = "vue3-temp-skill";
|
|
10
|
+
export const RUNTIME_DIRS = ["references", "scripts", "adapters", "mapping"];
|
|
11
|
+
// 保留已有改动:generate-page.mjs 不再分发;作者侧 cli、测试及临时产物不属于运行资源。
|
|
12
|
+
export const RUNTIME_SCRIPT_FILES = ["match.py", "modules.json", "check-landing.mjs", "generate-css.mjs", "verify-render.mjs"];
|
|
13
|
+
const RUNTIME_FILES = ["SKILL.md", "README.md"];
|
|
14
|
+
const MANIFEST = ".skill-install.json";
|
|
15
|
+
const BEGIN = `<!-- begin ${SKILL_NAME} -->`;
|
|
16
|
+
const END = `<!-- end ${SKILL_NAME} -->`;
|
|
17
|
+
const AUTO_DETECT = false;
|
|
18
|
+
|
|
19
|
+
// 可注入 HOME、包根和 cwd;import 本身不会安装、写记录或退出进程。
|
|
20
|
+
export function createInstaller({ home = os.homedir(), skillDir = PACKAGE_DIR, cwd = process.cwd(), log = console.log } = {}) {
|
|
21
|
+
home = path.resolve(home);
|
|
22
|
+
skillDir = path.resolve(skillDir);
|
|
23
|
+
cwd = path.resolve(cwd);
|
|
24
|
+
const version = JSON.parse(fs.readFileSync(path.join(skillDir, "package.json"), "utf8")).version;
|
|
25
|
+
const trackFile = path.join(home, `.${SKILL_NAME}.json`);
|
|
26
|
+
// 编辑器专属落点只属于安装器,不进入运行技能的协议或编排。
|
|
27
|
+
const targets = {
|
|
28
|
+
cursor: { name: "Cursor", type: "skills", dir: ".cursor/skills" },
|
|
29
|
+
qoder: { name: "Qoder", type: "skills", dir: ".qoder/skills" },
|
|
30
|
+
copilot: { name: "GitHub Copilot", type: "merge", file: ".github/copilot-instructions.md", projectOnly: true },
|
|
31
|
+
windsurf: { name: "Windsurf", type: "rules", dir: ".windsurf/rules" },
|
|
32
|
+
gemini: { name: "Gemini CLI", type: "merge", file: "GEMINI.md", projectOnly: true },
|
|
33
|
+
trae: { name: "Trae", type: "rules", dir: ".trae/rules" },
|
|
34
|
+
codebuddy: { name: "CodeBuddy", type: "rules", dir: ".codebuddy/rules" },
|
|
35
|
+
claude: { name: "Claude Code", type: "skills", dir: ".claude/skills" },
|
|
36
|
+
universal: { name: "Universal (.agents)", type: "skills", dir: ".agents/skills" },
|
|
37
|
+
};
|
|
38
|
+
const stat = (file) => {
|
|
39
|
+
try { return fs.lstatSync(file); } catch (err) { if (err.code === "ENOENT") return null; throw err; }
|
|
40
|
+
};
|
|
41
|
+
const conflict = (file) => { throw new Error(`未管理或类型不符的路径冲突,保留原内容: ${file}`); };
|
|
42
|
+
function safePath(file) {
|
|
43
|
+
for (let current = path.resolve(file); ; current = path.dirname(current)) {
|
|
44
|
+
const info = stat(current);
|
|
45
|
+
if (info?.isSymbolicLink() || (current !== path.resolve(file) && info && !info.isDirectory())) conflict(current);
|
|
46
|
+
if (path.dirname(current) === current) break;
|
|
47
|
+
}
|
|
130
48
|
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
function copyDir(src, dest, skipDirs = []) {
|
|
138
|
-
ensureDir(dest);
|
|
139
|
-
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
|
140
|
-
if (entry.name.startsWith(".") || entry.isSymbolicLink()) continue;
|
|
141
|
-
if (entry.isDirectory() && skipDirs.includes(entry.name)) continue;
|
|
142
|
-
const s = path.join(src, entry.name);
|
|
143
|
-
const d = path.join(dest, entry.name);
|
|
144
|
-
if (entry.isDirectory()) copyDir(s, d, skipDirs);
|
|
145
|
-
else fs.copyFileSync(s, d);
|
|
49
|
+
function projectPath(value) {
|
|
50
|
+
if (typeof value !== "string" || !value.trim() || value.startsWith("-")) throw new Error("--project 需要有效目录参数");
|
|
51
|
+
const dir = path.resolve(cwd, value);
|
|
52
|
+
safePath(dir);
|
|
53
|
+
if (!stat(dir)?.isDirectory()) throw new Error(`--project 必须是已存在的目录: ${dir}`);
|
|
54
|
+
return dir;
|
|
146
55
|
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
56
|
+
function paths(target, project = null) {
|
|
57
|
+
const cfg = targets[target];
|
|
58
|
+
if (!cfg) throw new Error(`未知目标: ${target}`);
|
|
59
|
+
if (cfg.projectOnly && !project) throw new Error(`${target} 仅支持项目安装,必须指定 --project <dir>`);
|
|
60
|
+
const root = project ? projectPath(project) : home;
|
|
61
|
+
const dest = cfg.file ? path.join(root, cfg.file) : path.join(root, cfg.dir, SKILL_NAME + (cfg.type === "rules" ? ".md" : ""));
|
|
62
|
+
// 文件旁独立资源避免多个编辑器共享生命周期;所有入口都是相对索引文件的路径。
|
|
63
|
+
const body = cfg.type === "skills" ? dest : path.join(path.dirname(dest), `.${SKILL_NAME}-resources`);
|
|
64
|
+
return { cfg, dest, body };
|
|
65
|
+
}
|
|
66
|
+
const excluded = (name) => name.startsWith(".") || /(?:^__pycache__$|^node_modules$|^fixtures?$|^(?:tmp|temp|coverage|test-results|playwright-report)$|(?:\.test|\.spec)\.|\.(?:tmp|temp|log|pyc|tgz)$|~$)/i.test(name);
|
|
67
|
+
function runtimeFiles() {
|
|
68
|
+
const files = new Map();
|
|
69
|
+
function add(relative) {
|
|
70
|
+
const source = path.join(skillDir, relative);
|
|
71
|
+
safePath(source);
|
|
72
|
+
if (!stat(source)?.isFile()) throw new Error(`运行资源缺失: ${source}`);
|
|
73
|
+
files.set(relative, fs.readFileSync(source));
|
|
157
74
|
}
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
75
|
+
function walk(relative) {
|
|
76
|
+
const source = path.join(skillDir, relative);
|
|
77
|
+
safePath(source);
|
|
78
|
+
if (!stat(source)?.isDirectory()) throw new Error(`运行资源目录缺失: ${source}`);
|
|
79
|
+
for (const entry of fs.readdirSync(source, { withFileTypes: true })) {
|
|
80
|
+
if (excluded(entry.name) || entry.isSymbolicLink()) continue;
|
|
81
|
+
const child = `${relative}/${entry.name}`;
|
|
82
|
+
if (entry.isDirectory()) walk(child);
|
|
83
|
+
else if (entry.isFile()) add(child);
|
|
84
|
+
}
|
|
164
85
|
}
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
* 读取技能完整内容:SKILL.md + references/*.md
|
|
170
|
-
*/
|
|
171
|
-
function readFullSkill() {
|
|
172
|
-
let content = "";
|
|
173
|
-
|
|
174
|
-
const skillMd = path.join(SKILL_DIR, "SKILL.md");
|
|
175
|
-
if (fs.existsSync(skillMd)) content += fs.readFileSync(skillMd, "utf-8");
|
|
176
|
-
|
|
177
|
-
const refsDir = path.join(SKILL_DIR, "references");
|
|
178
|
-
if (fs.existsSync(refsDir)) {
|
|
179
|
-
for (const f of fs.readdirSync(refsDir).filter((f) => f.endsWith(".md")).sort()) {
|
|
180
|
-
content += `\n\n---\n\n${fs.readFileSync(path.join(refsDir, f), "utf-8")}`;
|
|
86
|
+
for (const file of RUNTIME_FILES) add(file);
|
|
87
|
+
for (const dir of RUNTIME_DIRS) {
|
|
88
|
+
if (dir === "scripts") for (const file of RUNTIME_SCRIPT_FILES) add(`scripts/${file}`);
|
|
89
|
+
else walk(dir);
|
|
181
90
|
}
|
|
91
|
+
return files;
|
|
182
92
|
}
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
// 安装记录(用于 update / list / uninstall)
|
|
188
|
-
// ============================================================
|
|
189
|
-
|
|
190
|
-
function readTrack() {
|
|
191
|
-
try {
|
|
192
|
-
return JSON.parse(fs.readFileSync(TRACK_FILE, "utf-8"));
|
|
193
|
-
} catch {
|
|
194
|
-
return { installed: [], version: "" };
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
function writeTrack(data) {
|
|
199
|
-
fs.writeFileSync(TRACK_FILE, JSON.stringify(data, null, 2), "utf-8");
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
function recordInstall(targetKey, projectDir) {
|
|
203
|
-
const track = readTrack();
|
|
204
|
-
const key = projectDir ? `${targetKey}@${projectDir}` : targetKey;
|
|
205
|
-
if (!track.installed.includes(key)) {
|
|
206
|
-
track.installed.push(key);
|
|
207
|
-
}
|
|
208
|
-
track.version = PKG_VERSION;
|
|
209
|
-
track.updatedAt = new Date().toISOString();
|
|
210
|
-
writeTrack(track);
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
function recordUninstall(targetKey, projectDir) {
|
|
214
|
-
const track = readTrack();
|
|
215
|
-
const key = projectDir ? `${targetKey}@${projectDir}` : targetKey;
|
|
216
|
-
track.installed = track.installed.filter((k) => k !== key);
|
|
217
|
-
writeTrack(track);
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
// ============================================================
|
|
221
|
-
// 安装逻辑
|
|
222
|
-
// ============================================================
|
|
223
|
-
|
|
224
|
-
/**
|
|
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
|
|
239
|
-
*/
|
|
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
|
-
|
|
245
|
-
// 先清理旧版本
|
|
246
|
-
if (fs.existsSync(dest)) {
|
|
247
|
-
removeDir(dest);
|
|
93
|
+
function validRelative(file) {
|
|
94
|
+
return typeof file === "string" && !file.includes("\\") && !file.includes(":") && !path.posix.isAbsolute(file) &&
|
|
95
|
+
file.split("/").every((part) => part && part !== "." && part !== "..") &&
|
|
96
|
+
(RUNTIME_FILES.includes(file) || RUNTIME_DIRS.some((dir) => file.startsWith(`${dir}/`)));
|
|
248
97
|
}
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
98
|
+
// 无标记旧目录仅在 --force 且每个文件均可与本包核对时认领;旧版本无法核对则提示人工迁移。
|
|
99
|
+
function ownership(dir, force, source) {
|
|
100
|
+
safePath(dir);
|
|
101
|
+
if (!stat(dir)) return { files: [], exists: false };
|
|
102
|
+
if (!stat(dir).isDirectory()) conflict(dir);
|
|
103
|
+
const marker = path.join(dir, MANIFEST);
|
|
104
|
+
safePath(marker);
|
|
105
|
+
if (stat(marker)) {
|
|
106
|
+
if (!stat(marker).isFile()) conflict(marker);
|
|
107
|
+
let data;
|
|
108
|
+
try { data = JSON.parse(fs.readFileSync(marker, "utf8")); } catch { conflict(marker); }
|
|
109
|
+
if (data.schema !== 1 || data.skill !== SKILL_NAME || !Array.isArray(data.files) ||
|
|
110
|
+
!data.files.includes("SKILL.md") || !data.files.every(validRelative) || new Set(data.files).size !== data.files.length) conflict(marker);
|
|
111
|
+
for (const file of data.files) {
|
|
112
|
+
const dest = path.join(dir, file);
|
|
113
|
+
safePath(dest);
|
|
114
|
+
if (stat(dest) && !stat(dest).isFile()) conflict(dest);
|
|
115
|
+
}
|
|
116
|
+
return { files: data.files, exists: true };
|
|
117
|
+
}
|
|
118
|
+
if (!force) throw new Error(`未管理的旧目录冲突: ${dir};仅可用 --force 尝试识别自有资源后迁移`);
|
|
119
|
+
source ||= runtimeFiles();
|
|
120
|
+
const files = [];
|
|
121
|
+
function inspect(relative = "") {
|
|
122
|
+
for (const entry of fs.readdirSync(path.join(dir, relative), { withFileTypes: true })) {
|
|
123
|
+
const file = relative ? `${relative}/${entry.name}` : entry.name;
|
|
124
|
+
const full = path.join(dir, file);
|
|
125
|
+
if (entry.isSymbolicLink()) conflict(full);
|
|
126
|
+
if (entry.isDirectory()) {
|
|
127
|
+
if (![...source.keys()].some((key) => key.startsWith(`${file}/`))) conflict(full);
|
|
128
|
+
inspect(file);
|
|
129
|
+
} else if (entry.isFile() && source.has(file) && fs.readFileSync(full).equals(source.get(file))) files.push(file);
|
|
130
|
+
else conflict(full);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
inspect();
|
|
134
|
+
if (!files.includes("SKILL.md")) conflict(dir);
|
|
135
|
+
return { files, exists: true, legacy: true };
|
|
258
136
|
}
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
137
|
+
function blockRange(text) {
|
|
138
|
+
const start = text.indexOf(BEGIN);
|
|
139
|
+
const end = text.indexOf(END);
|
|
140
|
+
if (start === -1 && end === -1) return null;
|
|
141
|
+
if (start === -1 || end < start || text.indexOf(BEGIN, start + BEGIN.length) !== -1 || text.indexOf(END, end + END.length) !== -1 ||
|
|
142
|
+
/<!--\s*(?:begin|end)\s+[^>]+-->/.test(text.slice(start + BEGIN.length, end))) {
|
|
143
|
+
throw new Error(`损坏、重复或嵌套的 ${SKILL_NAME} begin/end 标记;未修改文件`);
|
|
266
144
|
}
|
|
145
|
+
return [start, end + END.length];
|
|
267
146
|
}
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
copyDir(mappingSrc, path.join(dest, "mapping"));
|
|
147
|
+
function loader(dest, body) {
|
|
148
|
+
const entry = `./${path.relative(path.dirname(dest), path.join(body, "SKILL.md")).split(path.sep).join("/")}`;
|
|
149
|
+
// 分隔空白全部在标记内,卸载可逐字恢复无尾换行的用户说明。
|
|
150
|
+
return `${BEGIN}\n\n## ${SKILL_NAME}\n\n读取 [技能入口](<${entry}>) 并执行;相对资源和脚本路径以该 SKILL.md 所在目录为基准,不以项目工作目录为基准。\n与其他技能同轮使用时保留各自职责,按入口中的协作顺序执行;不支持技能调用时直接读取入口。\n\n${END}`;
|
|
273
151
|
}
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
if (fs.existsSync(legacy)) {
|
|
279
|
-
try {
|
|
280
|
-
fs.unlinkSync(legacy);
|
|
281
|
-
} catch {
|
|
282
|
-
// 忽略清理失败
|
|
283
|
-
}
|
|
152
|
+
function legacyRulesText(body) {
|
|
153
|
+
let content = fs.readFileSync(path.join(skillDir, "SKILL.md"), "utf8");
|
|
154
|
+
for (const file of fs.readdirSync(path.join(skillDir, "references")).filter((f) => f.endsWith(".md")).sort()) {
|
|
155
|
+
content += `\n\n---\n\n${fs.readFileSync(path.join(skillDir, "references", file), "utf8")}`;
|
|
284
156
|
}
|
|
157
|
+
return `> 技能本体(scripts/adapters/mapping/references)已随附安装至: ${body}\n` +
|
|
158
|
+
`> 运行施工脚本时一律使用该路径,例如:\n` +
|
|
159
|
+
`> node ${body}/scripts/generate-css.mjs --project <项目根>\n` +
|
|
160
|
+
`> node ${body}/scripts/check-landing.mjs --project <项目根>\n\n` + content;
|
|
285
161
|
}
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
function installTarget(targetKey, projectDir) {
|
|
301
|
-
const cfg = TARGETS[targetKey];
|
|
302
|
-
if (cfg.type === "rules") installRules(targetKey, projectDir);
|
|
303
|
-
else if (cfg.type === "skills") installSkills(targetKey, projectDir);
|
|
304
|
-
else if (cfg.type === "merge") installMerge(targetKey, projectDir);
|
|
305
|
-
|
|
306
|
-
recordInstall(targetKey, projectDir);
|
|
307
|
-
return resolveTargetPath(targetKey, projectDir);
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
// ============================================================
|
|
311
|
-
// 卸载逻辑
|
|
312
|
-
// ============================================================
|
|
313
|
-
|
|
314
|
-
function uninstallTarget(targetKey, projectDir) {
|
|
315
|
-
const cfg = TARGETS[targetKey];
|
|
316
|
-
|
|
317
|
-
if (cfg.type === "rules") {
|
|
318
|
-
const targetDir = cfg.dir(projectDir);
|
|
319
|
-
const file = path.join(targetDir, `${SKILL_NAME}.md`);
|
|
320
|
-
if (fs.existsSync(file)) fs.unlinkSync(file);
|
|
321
|
-
} else if (cfg.type === "skills") {
|
|
322
|
-
const targetDir = cfg.dir(projectDir);
|
|
323
|
-
const dest = path.join(targetDir, SKILL_NAME);
|
|
324
|
-
if (fs.existsSync(dest)) removeDir(dest);
|
|
325
|
-
// 同时清理 Cursor 旧版 rules
|
|
326
|
-
if (cfg.legacyRulesFile) {
|
|
327
|
-
const legacy = cfg.legacyRulesFile(projectDir);
|
|
328
|
-
if (fs.existsSync(legacy)) {
|
|
329
|
-
try {
|
|
330
|
-
fs.unlinkSync(legacy);
|
|
331
|
-
} catch {
|
|
332
|
-
// 忽略
|
|
162
|
+
function indexState(cfg, dest, force, source) {
|
|
163
|
+
safePath(dest);
|
|
164
|
+
if (!stat(dest)) return { text: "", range: null };
|
|
165
|
+
if (stat(dest).isDirectory()) {
|
|
166
|
+
if (!force) throw new Error(`旧目录占用了索引文件: ${dest};需要 --force 并识别自有资源后迁移`);
|
|
167
|
+
const directory = ownership(dest, force, source);
|
|
168
|
+
// 索引目录必须可完整移走;不能先删除受管文件再发现其中还留有用户文件。
|
|
169
|
+
function check(relative = "") {
|
|
170
|
+
for (const entry of fs.readdirSync(path.join(dest, relative), { withFileTypes: true })) {
|
|
171
|
+
const file = relative ? `${relative}/${entry.name}` : entry.name;
|
|
172
|
+
if (entry.isDirectory() && directory.files.some((owned) => owned.startsWith(`${file}/`))) check(file);
|
|
173
|
+
else if (!entry.isFile() || (!directory.files.includes(file) && (directory.legacy || file !== MANIFEST))) conflict(path.join(dest, file));
|
|
333
174
|
}
|
|
334
175
|
}
|
|
176
|
+
check();
|
|
177
|
+
return { text: "", range: null, directory };
|
|
335
178
|
}
|
|
336
|
-
|
|
337
|
-
const
|
|
338
|
-
|
|
179
|
+
if (!stat(dest).isFile()) conflict(dest);
|
|
180
|
+
const text = fs.readFileSync(dest, "utf8");
|
|
181
|
+
const range = blockRange(text);
|
|
182
|
+
if (cfg.type === "rules" && !range) {
|
|
183
|
+
const legacyBody = path.join(path.dirname(path.dirname(dest)), "skills", SKILL_NAME);
|
|
184
|
+
if (!force || text !== legacyRulesText(legacyBody)) {
|
|
185
|
+
throw new Error(`未管理的规则文件冲突: ${dest};旧版本需 --force 且可识别,否则请人工迁移`);
|
|
186
|
+
}
|
|
187
|
+
return { text: "", range: null, legacyBody, legacyOwnership: ownership(legacyBody, force, source) };
|
|
188
|
+
}
|
|
189
|
+
return { text, range };
|
|
339
190
|
}
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
function showHelp() {
|
|
349
|
-
console.log(`
|
|
350
|
-
${bold("vue3-temp-skill")} v${PKG_VERSION}
|
|
351
|
-
Vue 3 企业级中后台项目开发规范技能包
|
|
352
|
-
|
|
353
|
-
${bold("用法:")}
|
|
354
|
-
vue3-temp-skill <command> [target] [options]
|
|
355
|
-
|
|
356
|
-
${bold("命令:")}
|
|
357
|
-
install <target> 安装技能到指定 AI 工具
|
|
358
|
-
update [target] 更新已安装的技能(默认更新全部已安装目标)
|
|
359
|
-
uninstall [target] 卸载技能(默认卸载全部已安装目标)
|
|
360
|
-
list 查看已安装的目标和版本
|
|
361
|
-
|
|
362
|
-
${bold("目标 (target):")}
|
|
363
|
-
cursor → Cursor ~/.cursor/skills/
|
|
364
|
-
qoder → Qoder ~/.qoderworkcn/skills/
|
|
365
|
-
copilot → GitHub Copilot <项目>/.github/copilot-instructions.md(项目级)
|
|
366
|
-
windsurf → Windsurf ~/.windsurf/rules/
|
|
367
|
-
gemini → Gemini CLI <项目>/GEMINI.md(项目级)
|
|
368
|
-
trae → Trae ~/.trae/rules/
|
|
369
|
-
claude → Claude Code ~/.claude/skills/
|
|
370
|
-
universal → 通用标准 ~/.agents/skills/
|
|
371
|
-
all → 安装到所有支持的工具
|
|
372
|
-
|
|
373
|
-
${bold("选项:")}
|
|
374
|
-
--project <dir> 安装到指定项目目录(默认全局安装到用户主目录)
|
|
375
|
-
--help, -h 显示帮助
|
|
376
|
-
--version, -v 显示版本号
|
|
377
|
-
|
|
378
|
-
${bold("示例:")}
|
|
379
|
-
${dim("# 安装到 Cursor(全局技能)")}
|
|
380
|
-
npx @qilitt-mickey/vue3-temp-skill install cursor
|
|
381
|
-
|
|
382
|
-
${dim("# 安装到所有工具(全局)+ 当前目录的 Copilot/Gemini 项目文件")}
|
|
383
|
-
npx @qilitt-mickey/vue3-temp-skill install all
|
|
384
|
-
|
|
385
|
-
${dim("# 安装到指定项目(含 .agents / Copilot / Gemini)")}
|
|
386
|
-
npx @qilitt-mickey/vue3-temp-skill install all --project ./my-app
|
|
387
|
-
|
|
388
|
-
${dim("# 更新所有已安装的目标(搭配 @latest 拉取最新版)")}
|
|
389
|
-
npx @qilitt-mickey/vue3-temp-skill@latest update
|
|
390
|
-
|
|
391
|
-
${dim("# 查看已安装情况")}
|
|
392
|
-
npx @qilitt-mickey/vue3-temp-skill list
|
|
393
|
-
`);
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
function showVersion() {
|
|
397
|
-
console.log(PKG_VERSION);
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
function warnProjectOnly(targetKey, projectDir) {
|
|
401
|
-
const cfg = TARGETS[targetKey];
|
|
402
|
-
if (cfg.projectOnly && !projectDir) {
|
|
403
|
-
console.log(
|
|
404
|
-
yellow(
|
|
405
|
-
` ! ${cfg.name} 为项目级配置,已写入当前目录: ${resolveTargetPath(targetKey, null)}`
|
|
406
|
-
)
|
|
407
|
-
);
|
|
408
|
-
console.log(dim(` 建议: vue3-temp-skill install ${targetKey} --project <项目路径>`));
|
|
191
|
+
function prune(dir, files) {
|
|
192
|
+
const directories = new Set([dir]);
|
|
193
|
+
for (const file of files) {
|
|
194
|
+
for (let parent = path.dirname(path.join(dir, file)); parent !== dir; parent = path.dirname(parent)) directories.add(parent);
|
|
195
|
+
}
|
|
196
|
+
for (const directory of [...directories].sort((a, b) => b.length - a.length)) {
|
|
197
|
+
if (stat(directory)?.isDirectory() && fs.readdirSync(directory).length === 0) fs.rmdirSync(directory);
|
|
198
|
+
}
|
|
409
199
|
}
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
console.error(red("✗ 请指定安装目标,例如: vue3-temp-skill install cursor"));
|
|
415
|
-
console.error("运行 vue3-temp-skill --help 查看所有支持的目标");
|
|
416
|
-
process.exit(1);
|
|
200
|
+
function removeBody(dir, owned) {
|
|
201
|
+
for (const file of owned.files) if (stat(path.join(dir, file))) fs.unlinkSync(path.join(dir, file));
|
|
202
|
+
if (!owned.legacy && stat(path.join(dir, MANIFEST))) fs.unlinkSync(path.join(dir, MANIFEST));
|
|
203
|
+
prune(dir, owned.files);
|
|
417
204
|
}
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
const
|
|
421
|
-
|
|
422
|
-
const
|
|
423
|
-
|
|
424
|
-
for (const
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
} catch (err) {
|
|
432
|
-
failed.push(TARGETS[key].name);
|
|
433
|
-
console.log(red(` ✗ ${TARGETS[key].name}: ${err.message}`));
|
|
205
|
+
function install(target, project, force) {
|
|
206
|
+
const { cfg, dest, body } = paths(target, project);
|
|
207
|
+
const source = runtimeFiles();
|
|
208
|
+
const index = cfg.type === "skills" ? null : indexState(cfg, dest, force, source);
|
|
209
|
+
const owned = ownership(body, force, source);
|
|
210
|
+
// 先完成全部冲突检查,不能写完资源后才发现用户索引不允许覆盖。
|
|
211
|
+
for (const file of source.keys()) {
|
|
212
|
+
const output = path.join(body, file);
|
|
213
|
+
safePath(output);
|
|
214
|
+
if (stat(output) && (!stat(output).isFile() || !owned.files.includes(file))) conflict(output);
|
|
215
|
+
for (let relative = path.posix.dirname(file); relative !== "."; relative = path.posix.dirname(relative)) {
|
|
216
|
+
const parent = path.join(body, relative);
|
|
217
|
+
if (stat(parent) && !owned.files.some((previous) => previous.startsWith(`${relative}/`))) conflict(parent);
|
|
434
218
|
}
|
|
435
219
|
}
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
console.log(yellow(`✓ 安装完成: ${success}/${total} 成功`));
|
|
441
|
-
console.log(red(` ✗ 失败: ${failed.join(", ")}`));
|
|
220
|
+
for (const [file, content] of source) {
|
|
221
|
+
const output = path.join(body, file);
|
|
222
|
+
fs.mkdirSync(path.dirname(output), { recursive: true });
|
|
223
|
+
fs.writeFileSync(output, content);
|
|
442
224
|
}
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
225
|
+
const stale = owned.files.filter((file) => !source.has(file));
|
|
226
|
+
for (const file of stale) if (stat(path.join(body, file))) fs.unlinkSync(path.join(body, file));
|
|
227
|
+
prune(body, stale);
|
|
228
|
+
fs.writeFileSync(path.join(body, MANIFEST), JSON.stringify({ schema: 1, skill: SKILL_NAME, files: [...source.keys()].sort() }, null, 2) + "\n");
|
|
229
|
+
if (index) {
|
|
230
|
+
if (index.directory) removeBody(dest, index.directory);
|
|
231
|
+
if (index.legacyBody) removeBody(index.legacyBody, index.legacyOwnership);
|
|
232
|
+
const block = loader(dest, body);
|
|
233
|
+
const text = index.range ? index.text.slice(0, index.range[0]) + block + index.text.slice(index.range[1]) : index.text + block;
|
|
234
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
235
|
+
fs.writeFileSync(dest, text, "utf8");
|
|
452
236
|
}
|
|
453
|
-
|
|
454
|
-
console.error(red(`✗ 未知目标: ${target}`));
|
|
455
|
-
console.error("运行 vue3-temp-skill --help 查看支持的目标");
|
|
456
|
-
process.exit(1);
|
|
237
|
+
return dest;
|
|
457
238
|
}
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
239
|
+
function uninstall(target, project, force) {
|
|
240
|
+
const { cfg, dest, body } = paths(target, project);
|
|
241
|
+
const index = cfg.type === "skills" ? null : indexState(cfg, dest, force);
|
|
242
|
+
const owned = ownership(body, force);
|
|
243
|
+
if (index?.directory) removeBody(dest, index.directory);
|
|
244
|
+
else if (index?.range) {
|
|
245
|
+
const text = index.text.slice(0, index.range[0]) + index.text.slice(index.range[1]);
|
|
246
|
+
if (!text && cfg.type === "rules") fs.unlinkSync(dest);
|
|
247
|
+
else fs.writeFileSync(dest, text, "utf8");
|
|
248
|
+
} else if (index?.legacyBody) fs.unlinkSync(dest);
|
|
249
|
+
if (index?.legacyBody) removeBody(index.legacyBody, index.legacyOwnership);
|
|
250
|
+
if (owned.exists) removeBody(body, owned);
|
|
251
|
+
return dest;
|
|
466
252
|
}
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
try {
|
|
479
|
-
installTarget(key, projectDir);
|
|
480
|
-
success++;
|
|
481
|
-
} catch (err) {
|
|
482
|
-
failed.push(TARGETS[key].name);
|
|
483
|
-
}
|
|
484
|
-
}
|
|
485
|
-
if (failed.length === 0) {
|
|
486
|
-
console.log(green(`✓ 已更新到 v${PKG_VERSION}`));
|
|
253
|
+
function readTrack() {
|
|
254
|
+
safePath(trackFile);
|
|
255
|
+
if (!stat(trackFile)) return [];
|
|
256
|
+
const data = JSON.parse(fs.readFileSync(trackFile, "utf8"));
|
|
257
|
+
let entries;
|
|
258
|
+
if (data.schema === 2 && Array.isArray(data.entries)) entries = data.entries;
|
|
259
|
+
else if (Array.isArray(data.installed)) {
|
|
260
|
+
entries = data.installed.map((entry) => {
|
|
261
|
+
const at = entry.indexOf("@");
|
|
262
|
+
return { target: at === -1 ? entry : entry.slice(0, at), project: at === -1 ? null : entry.slice(at + 1), version: data.version };
|
|
263
|
+
});
|
|
487
264
|
} else {
|
|
488
|
-
|
|
489
|
-
console.log(red(` ✗ 失败: ${failed.join(", ")}`));
|
|
265
|
+
entries = Object.entries(data).filter(([key]) => targets[key]).map(([target, value]) => ({ target, project: value.project || null, version: value.version }));
|
|
490
266
|
}
|
|
491
|
-
return;
|
|
267
|
+
return entries.filter((entry) => targets[entry.target]).map((entry) => ({ ...entry, project: entry.project ? path.resolve(cwd, entry.project) : null }));
|
|
492
268
|
}
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
269
|
+
function writeTrack(entries) {
|
|
270
|
+
safePath(trackFile);
|
|
271
|
+
fs.mkdirSync(home, { recursive: true });
|
|
272
|
+
fs.writeFileSync(trackFile, JSON.stringify({ schema: 2, entries }, null, 2) + "\n");
|
|
273
|
+
}
|
|
274
|
+
function parseArgs(argv) {
|
|
275
|
+
const opts = { positional: [], force: false };
|
|
276
|
+
for (let i = 0; i < argv.length; i++) {
|
|
277
|
+
const arg = argv[i];
|
|
278
|
+
if (arg === "--project") {
|
|
279
|
+
if (opts.project !== undefined) throw new Error("--project 不可重复");
|
|
280
|
+
opts.project = projectPath(argv[++i]);
|
|
281
|
+
} else if (arg === "--force") opts.force = true;
|
|
282
|
+
else if (arg === "--help" || arg === "-h") opts.help = true;
|
|
283
|
+
else if (arg === "--version" || arg === "-v") opts.version = true;
|
|
284
|
+
else if (arg.startsWith("-")) throw new Error(`未知参数: ${arg}`);
|
|
285
|
+
else opts.positional.push(arg);
|
|
509
286
|
}
|
|
287
|
+
return opts;
|
|
510
288
|
}
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
} else {
|
|
514
|
-
console.log(yellow(`✓ 更新完成: ${success}/${track.installed.length} 成功`));
|
|
515
|
-
console.log(red(` ✗ 失败: ${failed.join(", ")}`));
|
|
289
|
+
function help() {
|
|
290
|
+
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 不会被自动删除。`);
|
|
516
291
|
}
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
process.exit(1);
|
|
292
|
+
function run(argv) {
|
|
293
|
+
const opts = parseArgs(argv);
|
|
294
|
+
if (opts.help || (!opts.positional.length && !opts.version)) { help(); return; }
|
|
295
|
+
if (opts.version) { log(version); return; }
|
|
296
|
+
let [command, target] = opts.positional;
|
|
297
|
+
if (targets[command] || command === "all" || (AUTO_DETECT && command === "系统")) {
|
|
298
|
+
if (target) throw new Error("多余的位置参数");
|
|
299
|
+
target = command; command = "install";
|
|
526
300
|
}
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
failed.push(TARGETS[key].name);
|
|
536
|
-
}
|
|
301
|
+
if (opts.positional.length > 2) throw new Error("多余的位置参数");
|
|
302
|
+
if (!["install", "update", "uninstall", "list"].includes(command)) throw new Error(`未知命令: ${command}`);
|
|
303
|
+
if (target === "系统" && AUTO_DETECT) target = "all";
|
|
304
|
+
if (target && target !== "all" && !targets[target]) throw new Error(`未知目标: ${target}`);
|
|
305
|
+
if (command === "list") {
|
|
306
|
+
if (target || opts.project || opts.force) throw new Error("list 不接受目标或安装参数");
|
|
307
|
+
for (const entry of readTrack()) log(`${entry.target} v${entry.version} ${entry.project || home}`);
|
|
308
|
+
return;
|
|
537
309
|
}
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
310
|
+
let track = readTrack();
|
|
311
|
+
let jobs;
|
|
312
|
+
if (command === "install") {
|
|
313
|
+
if (!target && !AUTO_DETECT) throw new Error("请指定安装目标");
|
|
314
|
+
const keys = target && target !== "all" ? [target] : Object.keys(targets).filter((key) => {
|
|
315
|
+
const cfg = targets[key];
|
|
316
|
+
if (cfg.projectOnly) return Boolean(opts.project);
|
|
317
|
+
return !AUTO_DETECT || Boolean(stat(path.join(opts.project || home, cfg.dir)));
|
|
318
|
+
});
|
|
319
|
+
jobs = keys.map((key) => ({ target: key, project: opts.project || null }));
|
|
545
320
|
} else {
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
console.error(red(`✗ 卸载失败: ${failed.join(", ")}`));
|
|
550
|
-
}
|
|
321
|
+
jobs = track.filter((entry) => !target || target === "all" || entry.target === target);
|
|
322
|
+
if (opts.project !== undefined) jobs = jobs.map((entry) => ({ ...entry, project: opts.project }));
|
|
323
|
+
if (target && target !== "all" && (opts.project !== undefined || jobs.length === 0)) jobs = [{ target, project: opts.project || null }];
|
|
551
324
|
}
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
const [targetKey, projDir] = entry.includes("@")
|
|
564
|
-
? [entry.split("@")[0], entry.split("@").slice(1).join("@")]
|
|
565
|
-
: [entry, null];
|
|
566
|
-
|
|
567
|
-
if (TARGETS[targetKey]) {
|
|
568
|
-
try {
|
|
569
|
-
uninstallTarget(targetKey, projDir || projectDir);
|
|
570
|
-
success++;
|
|
571
|
-
} catch {
|
|
572
|
-
// 静默
|
|
573
|
-
}
|
|
325
|
+
const identity = (entry) => JSON.stringify([entry.target, entry.project]);
|
|
326
|
+
jobs = [...new Map(jobs.map((entry) => [identity(entry), entry])).values()];
|
|
327
|
+
// 参数与所有项目根在写入前校验;显式 --project 始终覆盖旧记录的 project。
|
|
328
|
+
for (const job of jobs) paths(job.target, job.project);
|
|
329
|
+
if (!jobs.length) { log("未找到需要处理的目标。"); return; }
|
|
330
|
+
for (const job of jobs) {
|
|
331
|
+
const dest = command === "uninstall" ? uninstall(job.target, job.project, opts.force) : install(job.target, job.project, opts.force);
|
|
332
|
+
track = track.filter((entry) => identity(entry) !== identity(job));
|
|
333
|
+
if (command !== "uninstall") track.push({ target: job.target, project: job.project, version, path: dest });
|
|
334
|
+
writeTrack(track);
|
|
335
|
+
log(`${command} ${job.target} → ${dest}`);
|
|
574
336
|
}
|
|
575
337
|
}
|
|
576
|
-
|
|
577
|
-
console.log(green(`✓ 已卸载全部 ${success} 个工具`));
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
function cmdList() {
|
|
581
|
-
const track = readTrack();
|
|
582
|
-
|
|
583
|
-
console.log(bold(`\nvue3-temp-skill v${track.version || "未安装"}`));
|
|
584
|
-
console.log(dim(`记录文件: ${TRACK_FILE}\n`));
|
|
585
|
-
|
|
586
|
-
if (track.installed.length === 0) {
|
|
587
|
-
console.log(yellow("当前没有已安装的目标"));
|
|
588
|
-
return;
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
console.log(bold("已安装的目标:"));
|
|
592
|
-
for (const entry of track.installed) {
|
|
593
|
-
const [targetKey, projDir] = entry.includes("@")
|
|
594
|
-
? [entry.split("@")[0], entry.split("@").slice(1).join("@")]
|
|
595
|
-
: [entry, null];
|
|
596
|
-
|
|
597
|
-
const cfg = TARGETS[targetKey];
|
|
598
|
-
const name = cfg ? cfg.name : targetKey;
|
|
599
|
-
const scope = projDir ? ` (项目: ${projDir})` : " (全局)";
|
|
600
|
-
const dest = cfg ? resolveTargetPath(targetKey, projDir) : "";
|
|
601
|
-
const exists = dest && fs.existsSync(dest);
|
|
602
|
-
const mark = exists ? green("•") : yellow("!");
|
|
603
|
-
console.log(` ${mark} ${name}${scope}`);
|
|
604
|
-
if (dest) console.log(dim(` ${dest}${exists ? "" : " (文件不存在)"}`));
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
if (track.updatedAt) {
|
|
608
|
-
console.log(dim(`\n最后更新: ${track.updatedAt}`));
|
|
609
|
-
}
|
|
338
|
+
return { run, paths, runtimeFiles };
|
|
610
339
|
}
|
|
611
340
|
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
function main() {
|
|
617
|
-
const args = process.argv.slice(2);
|
|
618
|
-
|
|
619
|
-
if (!args.length || args.includes("--help") || args.includes("-h")) {
|
|
620
|
-
showHelp();
|
|
621
|
-
process.exit(0);
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
if (args.includes("--version") || args.includes("-v")) {
|
|
625
|
-
showVersion();
|
|
626
|
-
process.exit(0);
|
|
627
|
-
}
|
|
628
|
-
|
|
629
|
-
const command = args[0];
|
|
630
|
-
const target = args[1];
|
|
631
|
-
const projectIdx = args.indexOf("--project");
|
|
632
|
-
const projectDir = projectIdx !== -1 ? path.resolve(args[projectIdx + 1]) : null;
|
|
633
|
-
|
|
634
|
-
switch (command) {
|
|
635
|
-
case "install":
|
|
636
|
-
cmdInstall(target, projectDir);
|
|
637
|
-
break;
|
|
638
|
-
case "update":
|
|
639
|
-
cmdUpdate(target === "--project" ? null : target, projectDir);
|
|
640
|
-
break;
|
|
641
|
-
case "uninstall":
|
|
642
|
-
cmdUninstall(target === "--project" ? null : target, projectDir);
|
|
643
|
-
break;
|
|
644
|
-
case "list":
|
|
645
|
-
cmdList();
|
|
646
|
-
break;
|
|
647
|
-
default:
|
|
648
|
-
// 兼容旧用法:直接传 target 名当作 install
|
|
649
|
-
if (TARGETS[command] || command === "all") {
|
|
650
|
-
cmdInstall(command, projectDir);
|
|
651
|
-
} else {
|
|
652
|
-
console.error(red(`✗ 未知命令: ${command}`));
|
|
653
|
-
console.error("运行 vue3-temp-skill --help 查看帮助");
|
|
654
|
-
process.exit(1);
|
|
655
|
-
}
|
|
656
|
-
}
|
|
341
|
+
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
342
|
+
try { createInstaller().run(process.argv.slice(2)); }
|
|
343
|
+
catch (err) { console.error(err.message); process.exitCode = 1; }
|
|
657
344
|
}
|
|
658
|
-
|
|
659
|
-
main();
|