@tanlinfeng/project-docs-workflow 0.0.0-stage → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/NOTICE +52 -0
- package/README.md +162 -2
- package/bin/install.js +695 -0
- package/package.json +20 -4
- package/skills/deepwork/SKILL.md +15 -0
- package/skills/doc-opt/EXAMPLES.md +77 -0
- package/skills/doc-opt/SKILL.md +83 -0
- package/skills/doc-opt/agents/openai.yaml +5 -0
- package/skills/domain-modeling/SKILL.md +83 -0
- package/skills/domain-modeling/agents/openai.yaml +3 -0
- package/skills/grill-me/SKILL.md +13 -0
- package/skills/grill-with-docs/SKILL.md +18 -0
- package/skills/grilling/SKILL.md +46 -0
- package/skills/grilling/agents/openai.yaml +3 -0
- package/skills/improve-codebase-architecture/SKILL.md +15 -0
- package/skills/project-docs-workflow/SKILL.md +108 -0
- package/skills/project-docs-workflow/agents/openai.yaml +5 -0
- package/skills/project-docs-workflow/assets/templates/entries/adr-entry.md +27 -0
- package/skills/project-docs-workflow/assets/templates/entries/issue-entry.md +28 -0
- package/skills/project-docs-workflow/assets/templates/entries/pdr-current.md +19 -0
- package/skills/project-docs-workflow/assets/templates/entries/pdr-interfaces.md +28 -0
- package/skills/project-docs-workflow/assets/templates/entries/pdr-module-readme.md +34 -0
- package/skills/project-docs-workflow/assets/templates/entries/pdr-nfr.md +22 -0
- package/skills/project-docs-workflow/assets/templates/entries/pdr-spec.md +20 -0
- package/skills/project-docs-workflow/assets/templates/entries/term-entry.md +22 -0
- package/skills/project-docs-workflow/assets/templates/project/adr-readme.md +31 -0
- package/skills/project-docs-workflow/assets/templates/project/agents-block.md +15 -0
- package/skills/project-docs-workflow/assets/templates/project/agents-domain.md +44 -0
- package/skills/project-docs-workflow/assets/templates/project/agents-issues.md +65 -0
- package/skills/project-docs-workflow/assets/templates/project/agents-workflow.md +61 -0
- package/skills/project-docs-workflow/assets/templates/project/context.md +21 -0
- package/skills/project-docs-workflow/assets/templates/project/pdr-00-readme.md +36 -0
- package/skills/project-docs-workflow/references/adr.md +53 -0
- package/skills/project-docs-workflow/references/coexistence.md +49 -0
- package/skills/project-docs-workflow/references/deps/boundaries.md +29 -0
- package/skills/project-docs-workflow/references/deps/doc-opt.md +17 -0
- package/skills/project-docs-workflow/references/deps/domain-modeling.md +51 -0
- package/skills/project-docs-workflow/references/deps/grilling.md +31 -0
- package/skills/project-docs-workflow/references/deps/tdd.md +35 -0
- package/skills/project-docs-workflow/references/issues.md +51 -0
- package/skills/project-docs-workflow/references/pdr.md +33 -0
- package/skills/project-docs-workflow/references/terminology.md +21 -0
- package/skills/project-docs-workflow/references/workflow.md +45 -0
- package/skills/retro/SKILL.md +19 -0
- package/skills/tdd/SKILL.md +50 -0
- package/skills/tdd/agents/openai.yaml +3 -0
package/bin/install.js
ADDED
|
@@ -0,0 +1,695 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
const fs = require("fs");
|
|
5
|
+
const os = require("os");
|
|
6
|
+
const path = require("path");
|
|
7
|
+
const { spawnSync } = require("child_process");
|
|
8
|
+
|
|
9
|
+
const PKG_ROOT = path.join(__dirname, "..");
|
|
10
|
+
const SKILLS_DIR = path.join(PKG_ROOT, "skills");
|
|
11
|
+
const MAIN_SKILL = "project-docs-workflow";
|
|
12
|
+
const DEFAULT_TARGET = path.join(os.homedir(), ".agents", "skills");
|
|
13
|
+
// 安装清单:记录本包每次安装处理过的 skill 及其归属,供 uninstall 精确卸载。
|
|
14
|
+
const MANIFEST_NAME = ".project-docs-workflow.install.json";
|
|
15
|
+
const PKG_VERSION = require(path.join(PKG_ROOT, "package.json")).version;
|
|
16
|
+
|
|
17
|
+
// 可选上游:mattpocock/skills 的完整版依赖 skill。
|
|
18
|
+
// 官方安装器是 vercel-labs/skills 提供的 `skills` CLI(npm 包 skills),
|
|
19
|
+
// 固定以 `-a <agent> -g` 安装到该 agent 的全局目录(codex 为 ~/.agents/skills)。
|
|
20
|
+
const UPSTREAM_REPO = "mattpocock/skills";
|
|
21
|
+
const UPSTREAM_DEFAULT_AGENT = "codex";
|
|
22
|
+
// 上游安装器要求 Node >= 22.20(本项目不引入 semver 依赖,手工比较 major/minor)。
|
|
23
|
+
const UPSTREAM_MIN_NODE = { major: 22, minor: 20 };
|
|
24
|
+
// 仅这 8 个依赖 skill 属于上游集合;doc-opt 与主 skill 不属于。
|
|
25
|
+
const MATT_DEP_NAMES = [
|
|
26
|
+
"grilling",
|
|
27
|
+
"grill-me",
|
|
28
|
+
"grill-with-docs",
|
|
29
|
+
"domain-modeling",
|
|
30
|
+
"tdd",
|
|
31
|
+
"deepwork",
|
|
32
|
+
"retro",
|
|
33
|
+
"improve-codebase-architecture",
|
|
34
|
+
];
|
|
35
|
+
const MATT_DEP_SET = new Set(MATT_DEP_NAMES);
|
|
36
|
+
|
|
37
|
+
// 依赖 skill 的按名解析与 ref 兜底映射。
|
|
38
|
+
// 顺序:先按名解析 <target>/<name>/SKILL.md,其次尝试 ref 兜底文件。
|
|
39
|
+
// grill-with-docs 的兜底包含 grilling.md 与 domain-modeling.md 两个文件。
|
|
40
|
+
const REF_FALLBACKS = {
|
|
41
|
+
grilling: ["grilling.md"],
|
|
42
|
+
"grill-me": ["grilling.md"],
|
|
43
|
+
"grill-with-docs": ["grilling.md", "domain-modeling.md"],
|
|
44
|
+
"domain-modeling": ["domain-modeling.md"],
|
|
45
|
+
tdd: ["tdd.md"],
|
|
46
|
+
deepwork: ["boundaries.md"],
|
|
47
|
+
retro: ["boundaries.md"],
|
|
48
|
+
"doc-opt": ["doc-opt.md"],
|
|
49
|
+
"improve-codebase-architecture": ["boundaries.md"],
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const REF_NAMES = Object.keys(REF_FALLBACKS);
|
|
53
|
+
const DEPS_DIR = path.join(SKILLS_DIR, MAIN_SKILL, "references", "deps");
|
|
54
|
+
|
|
55
|
+
function usage() {
|
|
56
|
+
console.log(
|
|
57
|
+
[
|
|
58
|
+
"project-docs-workflow 安装器",
|
|
59
|
+
"",
|
|
60
|
+
"用法:",
|
|
61
|
+
" project-docs-workflow [install] [选项] 安装 skill 到目标目录(默认动作)",
|
|
62
|
+
" project-docs-workflow check [选项] 检查依赖 skill 的解析状态",
|
|
63
|
+
" project-docs-workflow uninstall [选项] 依据安装清单卸载本包安装的 skill",
|
|
64
|
+
"",
|
|
65
|
+
"选项:",
|
|
66
|
+
" --dir <path> 指定目标 skills 根目录(默认 ~/.agents/skills)",
|
|
67
|
+
" --force 覆盖已存在的 project-docs-workflow(仅主 skill;依赖仍不覆盖);",
|
|
68
|
+
" uninstall 时为非交互确认(非 TTY 下必须显式传入)",
|
|
69
|
+
" --dry-run 仅打印将执行的操作,不实际写入",
|
|
70
|
+
" --with-matt-skills",
|
|
71
|
+
" 可选:改从上游 mattpocock/skills 安装依赖 skill 的完整版",
|
|
72
|
+
" (需 Node >= 22.20;无此 flag 时在交互式终端会询问一次,默认否)",
|
|
73
|
+
" --matt-agent <name>",
|
|
74
|
+
" 上游安装使用的 agent(默认 codex,落盘到其全局目录 ~/.agents/skills)",
|
|
75
|
+
" -h, --help 显示本帮助",
|
|
76
|
+
].join("\n")
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function parseArgs(argv) {
|
|
81
|
+
const opts = {
|
|
82
|
+
command: "install",
|
|
83
|
+
target: DEFAULT_TARGET,
|
|
84
|
+
force: false,
|
|
85
|
+
dryRun: false,
|
|
86
|
+
withMattSkills: false,
|
|
87
|
+
mattAgent: UPSTREAM_DEFAULT_AGENT,
|
|
88
|
+
};
|
|
89
|
+
const positional = [];
|
|
90
|
+
for (let i = 0; i < argv.length; i++) {
|
|
91
|
+
const arg = argv[i];
|
|
92
|
+
if (arg === "install" || arg === "check" || arg === "uninstall") {
|
|
93
|
+
opts.command = arg;
|
|
94
|
+
} else if (arg === "--force") {
|
|
95
|
+
opts.force = true;
|
|
96
|
+
} else if (arg === "--dry-run") {
|
|
97
|
+
opts.dryRun = true;
|
|
98
|
+
} else if (arg === "--with-matt-skills") {
|
|
99
|
+
opts.withMattSkills = true;
|
|
100
|
+
} else if (arg === "--matt-agent") {
|
|
101
|
+
const value = argv[++i];
|
|
102
|
+
if (!value) {
|
|
103
|
+
fail("--matt-agent 需要一个参数");
|
|
104
|
+
}
|
|
105
|
+
opts.mattAgent = value;
|
|
106
|
+
} else if (arg.startsWith("--matt-agent=")) {
|
|
107
|
+
const value = arg.slice("--matt-agent=".length);
|
|
108
|
+
if (!value) {
|
|
109
|
+
fail("--matt-agent 需要一个参数");
|
|
110
|
+
}
|
|
111
|
+
opts.mattAgent = value;
|
|
112
|
+
} else if (arg === "--dir") {
|
|
113
|
+
const value = argv[++i];
|
|
114
|
+
if (!value) {
|
|
115
|
+
fail("--dir 需要一个路径参数");
|
|
116
|
+
}
|
|
117
|
+
opts.target = path.resolve(value);
|
|
118
|
+
} else if (arg.startsWith("--dir=")) {
|
|
119
|
+
opts.target = path.resolve(arg.slice("--dir=".length));
|
|
120
|
+
} else if (arg === "-h" || arg === "--help") {
|
|
121
|
+
usage();
|
|
122
|
+
process.exit(0);
|
|
123
|
+
} else if (arg.startsWith("-")) {
|
|
124
|
+
fail("未知选项:" + arg);
|
|
125
|
+
} else {
|
|
126
|
+
positional.push(arg);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
if (positional.length > 0) {
|
|
130
|
+
fail("未知参数:" + positional.join(" "));
|
|
131
|
+
}
|
|
132
|
+
return opts;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function fail(message) {
|
|
136
|
+
console.error("错误:" + message);
|
|
137
|
+
process.exit(1);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function isDir(p) {
|
|
141
|
+
try {
|
|
142
|
+
return fs.statSync(p).isDirectory();
|
|
143
|
+
} catch (e) {
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function isFile(p) {
|
|
149
|
+
try {
|
|
150
|
+
return fs.statSync(p).isFile();
|
|
151
|
+
} catch (e) {
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// —— 上游(mattpocock/skills)相关辅助 ——
|
|
157
|
+
|
|
158
|
+
function existsAt(p) {
|
|
159
|
+
return isDir(p) || isFile(p);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// 手工解析 process.versions.node 并与 22.20 比较(不引入 semver 依赖)。
|
|
163
|
+
function nodeMeetsUpstream() {
|
|
164
|
+
const parts = String(process.versions.node).split(".");
|
|
165
|
+
const major = parseInt(parts[0], 10) || 0;
|
|
166
|
+
const minor = parseInt(parts[1], 10) || 0;
|
|
167
|
+
if (major !== UPSTREAM_MIN_NODE.major) {
|
|
168
|
+
return major > UPSTREAM_MIN_NODE.major;
|
|
169
|
+
}
|
|
170
|
+
return minor >= UPSTREAM_MIN_NODE.minor;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// 同步等待若干毫秒(用于 stdin 非阻塞时的短暂重试,避免忙等)。
|
|
174
|
+
function sleepSync(ms) {
|
|
175
|
+
try {
|
|
176
|
+
const sab = new SharedArrayBuffer(4);
|
|
177
|
+
Atomics.wait(new Int32Array(sab), 0, 0, ms);
|
|
178
|
+
} catch (e) {
|
|
179
|
+
/* 忽略:无法等待时直接继续 */
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// 同步读取 stdin 的一行。仅在已确认 stdin 为 TTY 时调用。
|
|
184
|
+
function readLineSync() {
|
|
185
|
+
const buf = Buffer.alloc(1);
|
|
186
|
+
let line = "";
|
|
187
|
+
for (;;) {
|
|
188
|
+
let bytesRead = 0;
|
|
189
|
+
try {
|
|
190
|
+
bytesRead = fs.readSync(0, buf, 0, 1, null);
|
|
191
|
+
} catch (err) {
|
|
192
|
+
if (err && err.code === "EAGAIN") {
|
|
193
|
+
sleepSync(10);
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
if (err && (err.code === "EOF" || err.code === "ENXIO")) {
|
|
197
|
+
return line;
|
|
198
|
+
}
|
|
199
|
+
throw err;
|
|
200
|
+
}
|
|
201
|
+
if (bytesRead === 0) {
|
|
202
|
+
return line;
|
|
203
|
+
}
|
|
204
|
+
const ch = buf.toString("utf8", 0, 1);
|
|
205
|
+
if (ch === "\n") {
|
|
206
|
+
return line;
|
|
207
|
+
}
|
|
208
|
+
if (ch !== "\r") {
|
|
209
|
+
line += ch;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function npxCommand() {
|
|
215
|
+
return process.platform === "win32" ? "npx.cmd" : "npx";
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// 组装上游命令参数(不含可执行文件名),以便打印或执行。
|
|
219
|
+
function buildUpstreamArgs(opts, names) {
|
|
220
|
+
return ["--yes", "skills@latest", "add", UPSTREAM_REPO, "-g", "-y", "-a", opts.mattAgent, "-s"].concat(
|
|
221
|
+
names
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function quoteArg(arg) {
|
|
226
|
+
return /[\s"]/.test(arg) ? '"' + arg.replace(/"/g, '\\"') + '"' : arg;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function upstreamCommandString(opts, names) {
|
|
230
|
+
return [npxCommand()].concat(buildUpstreamArgs(opts, names)).map(quoteArg).join(" ");
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// 决定是否启用上游安装。仅在 install 且非 dry-run、且 stdin/stdout 均为 TTY 时提示。
|
|
234
|
+
function decideUpstream(opts) {
|
|
235
|
+
if (opts.withMattSkills) {
|
|
236
|
+
return true;
|
|
237
|
+
}
|
|
238
|
+
if (opts.command !== "install" || opts.dryRun) {
|
|
239
|
+
return false;
|
|
240
|
+
}
|
|
241
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
242
|
+
return false;
|
|
243
|
+
}
|
|
244
|
+
process.stdout.write("是否从上游安装依赖 skill 的完整版(mattpocock/skills)?[y/N] ");
|
|
245
|
+
let answer = "";
|
|
246
|
+
try {
|
|
247
|
+
answer = readLineSync().trim().toLowerCase();
|
|
248
|
+
} catch (e) {
|
|
249
|
+
answer = "";
|
|
250
|
+
}
|
|
251
|
+
return answer === "y" || answer === "yes";
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// 列出 skills/ 下的所有子目录名;缺失时返回空数组(由调用方给出提示)。
|
|
255
|
+
function listSkillDirs() {
|
|
256
|
+
if (!isDir(SKILLS_DIR)) {
|
|
257
|
+
return null;
|
|
258
|
+
}
|
|
259
|
+
return fs
|
|
260
|
+
.readdirSync(SKILLS_DIR, { withFileTypes: true })
|
|
261
|
+
.filter((entry) => entry.isDirectory())
|
|
262
|
+
.map((entry) => entry.name)
|
|
263
|
+
.sort();
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function copySkill(name, dest, dryRun) {
|
|
267
|
+
const src = path.join(SKILLS_DIR, name);
|
|
268
|
+
if (dryRun) {
|
|
269
|
+
console.log(" [dry-run] 将复制 " + name + " → " + dest);
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
fs.cpSync(src, dest, { recursive: true });
|
|
273
|
+
console.log(" ✓ 已安装 " + name + " → " + dest);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// —— 安装清单(manifest)——
|
|
277
|
+
|
|
278
|
+
function manifestPath(target) {
|
|
279
|
+
return path.join(target, MANIFEST_NAME);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function loadManifest(target) {
|
|
283
|
+
const p = manifestPath(target);
|
|
284
|
+
if (!isFile(p)) {
|
|
285
|
+
return null;
|
|
286
|
+
}
|
|
287
|
+
try {
|
|
288
|
+
const data = JSON.parse(fs.readFileSync(p, "utf8"));
|
|
289
|
+
if (data && typeof data === "object" && data.entries && typeof data.entries === "object") {
|
|
290
|
+
return data;
|
|
291
|
+
}
|
|
292
|
+
} catch (e) {
|
|
293
|
+
/* 忽略:清单损坏时按缺失处理 */
|
|
294
|
+
}
|
|
295
|
+
return null;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// 合并新旧条目:归属一旦记在本包名下(installed/forced),后续因「已存在」而 skip 时不被降级;
|
|
299
|
+
// 而初次即为 skipped(安装时已存在、非本包安装)的名字始终保持 skipped。
|
|
300
|
+
function mergeEntries(prev, fresh) {
|
|
301
|
+
const out = Object.assign({}, prev);
|
|
302
|
+
for (const name of Object.keys(fresh)) {
|
|
303
|
+
const f = fresh[name];
|
|
304
|
+
const p = out[name];
|
|
305
|
+
if (f.action === "skipped" && p && p.action !== "skipped") {
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
out[name] = f;
|
|
309
|
+
}
|
|
310
|
+
return out;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function writeManifest(target, entries) {
|
|
314
|
+
const p = manifestPath(target);
|
|
315
|
+
const data = {
|
|
316
|
+
version: PKG_VERSION,
|
|
317
|
+
target: path.resolve(target),
|
|
318
|
+
updatedAt: new Date().toISOString(),
|
|
319
|
+
entries,
|
|
320
|
+
};
|
|
321
|
+
fs.writeFileSync(p, JSON.stringify(data, null, 2) + "\n");
|
|
322
|
+
console.log("已写入安装清单:" + p);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function removeManifest(target) {
|
|
326
|
+
const p = manifestPath(target);
|
|
327
|
+
if (!isFile(p)) {
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
try {
|
|
331
|
+
fs.rmSync(p, { force: true });
|
|
332
|
+
console.log("已删除安装清单:" + p);
|
|
333
|
+
} catch (e) {
|
|
334
|
+
console.log("⚠ 无法删除安装清单:" + p + "(" + e.message + ")");
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// 依赖 skill 的 kind:doc-opt 单列,其余依赖(含其它 skills/* 目录)视为 lite。
|
|
339
|
+
function depKind(name) {
|
|
340
|
+
return name === "doc-opt" ? "doc-opt" : "lite";
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// —— 上游卸载命令组装 ——
|
|
344
|
+
|
|
345
|
+
function buildUpstreamRemoveArgs(names, agent) {
|
|
346
|
+
return ["--yes", "skills@latest", "remove"].concat(names).concat(["-g", "-y", "-a", agent]);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function upstreamRemoveString(names, agent) {
|
|
350
|
+
return [npxCommand()].concat(buildUpstreamRemoveArgs(names, agent)).map(quoteArg).join(" ");
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function groupByAgent(list) {
|
|
354
|
+
const map = new Map();
|
|
355
|
+
for (const item of list) {
|
|
356
|
+
if (!map.has(item.agent)) {
|
|
357
|
+
map.set(item.agent, []);
|
|
358
|
+
}
|
|
359
|
+
map.get(item.agent).push(item.name);
|
|
360
|
+
}
|
|
361
|
+
return map;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// 优先委托上游 skills CLI 卸载;不可用时(Node 过旧 / npx 缺失 / 非 0 退出)回退直接删除目录。
|
|
365
|
+
function removeUpstreamWithFallback(names, agent, target) {
|
|
366
|
+
const directFallback = (reason) => {
|
|
367
|
+
console.log(" ⚠ " + reason);
|
|
368
|
+
for (const name of names) {
|
|
369
|
+
const dest = path.join(target, name);
|
|
370
|
+
if (existsAt(dest)) {
|
|
371
|
+
fs.rmSync(dest, { recursive: true, force: true });
|
|
372
|
+
console.log(" ✓ 已直接删除 " + name);
|
|
373
|
+
} else {
|
|
374
|
+
console.log(" - 未找到 " + name + "(跳过)");
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
console.log(
|
|
378
|
+
" 注意:上游锁文件 ~/.agents/.skill-lock.json 可能残留过期条目;可用 `npx skills@latest list` 核对。"
|
|
379
|
+
);
|
|
380
|
+
};
|
|
381
|
+
|
|
382
|
+
if (!nodeMeetsUpstream()) {
|
|
383
|
+
directFallback("当前 Node " + process.versions.node + " < 22.20,无法运行上游卸载器。");
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
console.log(" 将执行:" + upstreamRemoveString(names, agent));
|
|
387
|
+
const res = spawnSync(npxCommand(), buildUpstreamRemoveArgs(names, agent), {
|
|
388
|
+
stdio: "inherit",
|
|
389
|
+
env: Object.assign({}, process.env, { DISABLE_TELEMETRY: "1", DO_NOT_TRACK: "1" }),
|
|
390
|
+
});
|
|
391
|
+
if (res.error) {
|
|
392
|
+
directFallback("上游卸载无法执行(" + (res.error.code || res.error.message) + ")。");
|
|
393
|
+
} else if (res.status !== 0) {
|
|
394
|
+
directFallback("上游卸载失败(退出码 " + res.status + ")。");
|
|
395
|
+
} else {
|
|
396
|
+
console.log(" ✓ 已通过 skills CLI 卸载:" + names.join("、"));
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function runInstall(opts) {
|
|
401
|
+
const dirs = listSkillDirs();
|
|
402
|
+
if (dirs === null || dirs.length === 0) {
|
|
403
|
+
fail(
|
|
404
|
+
"找不到 skills 目录(" +
|
|
405
|
+
SKILLS_DIR +
|
|
406
|
+
")。请确认已在完整的 npm 包内运行(不要单独复制 bin/)。"
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
if (dirs.indexOf(MAIN_SKILL) === -1) {
|
|
410
|
+
fail("缺少主 skill 目录:skills/" + MAIN_SKILL);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// 是否启用上游完整版安装(可能触发一次交互式询问,仅限 TTY + install + 非 dry-run)。
|
|
414
|
+
const upstreamEnabled = decideUpstream(opts);
|
|
415
|
+
|
|
416
|
+
const nowIso = new Date().toISOString();
|
|
417
|
+
const freshEntries = {};
|
|
418
|
+
const record = (name, entry) => {
|
|
419
|
+
freshEntries[name] = Object.assign({ at: nowIso }, entry);
|
|
420
|
+
};
|
|
421
|
+
|
|
422
|
+
const target = opts.target;
|
|
423
|
+
console.log("目标目录:" + target + (opts.dryRun ? "(dry-run,不会写入)" : ""));
|
|
424
|
+
if (!opts.dryRun) {
|
|
425
|
+
fs.mkdirSync(target, { recursive: true });
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// 主 skill:仅在 --force 时覆盖已存在的版本,否则同样保护用户版本。
|
|
429
|
+
const mainDest = path.join(target, MAIN_SKILL);
|
|
430
|
+
const mainExists = isDir(mainDest) || isFile(mainDest);
|
|
431
|
+
console.log("主 skill:" + MAIN_SKILL);
|
|
432
|
+
if (mainExists && !opts.force) {
|
|
433
|
+
console.log(" 同名 skill 已存在,保留用户版本:" + MAIN_SKILL + "(如需覆盖请加 --force)");
|
|
434
|
+
record(MAIN_SKILL, { kind: "main", action: "skipped" });
|
|
435
|
+
} else if (mainExists && opts.force) {
|
|
436
|
+
if (opts.dryRun) {
|
|
437
|
+
console.log(" [dry-run] 将覆盖 " + MAIN_SKILL + " → " + mainDest);
|
|
438
|
+
} else {
|
|
439
|
+
fs.rmSync(mainDest, { recursive: true, force: true });
|
|
440
|
+
copySkill(MAIN_SKILL, mainDest, false);
|
|
441
|
+
}
|
|
442
|
+
record(MAIN_SKILL, { kind: "main", action: "forced" });
|
|
443
|
+
} else {
|
|
444
|
+
copySkill(MAIN_SKILL, mainDest, opts.dryRun);
|
|
445
|
+
record(MAIN_SKILL, { kind: "main", action: "installed" });
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// 可选:先从上游安装 8 个依赖 skill 的完整版。
|
|
449
|
+
// upstreamHandled 记录「本次由上游负责」的名字,这些名字不再复制本包轻量版。
|
|
450
|
+
let upstreamHandled = new Set();
|
|
451
|
+
if (upstreamEnabled) {
|
|
452
|
+
console.log("上游依赖 skill(mattpocock/skills 完整版):");
|
|
453
|
+
if (opts.target !== DEFAULT_TARGET) {
|
|
454
|
+
console.log(
|
|
455
|
+
" ⚠ 检测到自定义 --dir:上游安装固定使用 agent 全局目录(" +
|
|
456
|
+
opts.mattAgent +
|
|
457
|
+
" → ~/.agents/skills),无法跟随 --dir;--dir 下的轻量版仍按原逻辑处理。"
|
|
458
|
+
);
|
|
459
|
+
}
|
|
460
|
+
const preexisting = MATT_DEP_NAMES.filter((name) => existsAt(path.join(target, name)));
|
|
461
|
+
const missing = MATT_DEP_NAMES.filter((name) => preexisting.indexOf(name) === -1);
|
|
462
|
+
if (preexisting.length > 0) {
|
|
463
|
+
console.log(" 已存在,保留且不提交上游:" + preexisting.join("、"));
|
|
464
|
+
}
|
|
465
|
+
if (missing.length === 0) {
|
|
466
|
+
console.log(" 8 个依赖 skill 均已在目标目录,无需上游安装。");
|
|
467
|
+
} else if (opts.dryRun) {
|
|
468
|
+
console.log(" [dry-run] 将执行:" + upstreamCommandString(opts, missing));
|
|
469
|
+
upstreamHandled = new Set(missing);
|
|
470
|
+
} else if (!nodeMeetsUpstream()) {
|
|
471
|
+
console.log(
|
|
472
|
+
" ⚠ 当前 Node " +
|
|
473
|
+
process.versions.node +
|
|
474
|
+
" < 22.20,无法运行上游安装器;将对缺失项回退轻量版。"
|
|
475
|
+
);
|
|
476
|
+
} else {
|
|
477
|
+
console.log(" 将执行:" + upstreamCommandString(opts, missing));
|
|
478
|
+
const res = spawnSync(npxCommand(), buildUpstreamArgs(opts, missing), {
|
|
479
|
+
stdio: "inherit",
|
|
480
|
+
env: Object.assign({}, process.env, { DISABLE_TELEMETRY: "1", DO_NOT_TRACK: "1" }),
|
|
481
|
+
});
|
|
482
|
+
if (res.error) {
|
|
483
|
+
console.log(
|
|
484
|
+
" ⚠ 上游安装无法执行(" +
|
|
485
|
+
(res.error.code || res.error.message) +
|
|
486
|
+
");将对缺失项回退轻量版。"
|
|
487
|
+
);
|
|
488
|
+
} else if (res.status !== 0) {
|
|
489
|
+
console.log(" ⚠ 上游安装失败(退出码 " + res.status + ");将对缺失项回退轻量版。");
|
|
490
|
+
}
|
|
491
|
+
// 复核:仅把「本次新增」的名字标记为已由上游处理;已存在者走下面的保留逻辑。
|
|
492
|
+
const nowPresent = MATT_DEP_NAMES.filter((name) => existsAt(path.join(target, name)));
|
|
493
|
+
upstreamHandled = new Set(nowPresent.filter((name) => missing.indexOf(name) !== -1));
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// 其他依赖 skill:永不覆盖。
|
|
498
|
+
console.log("依赖 skill:");
|
|
499
|
+
const others = dirs.filter((name) => name !== MAIN_SKILL);
|
|
500
|
+
if (others.length === 0) {
|
|
501
|
+
console.log(" (未发现其他目录)");
|
|
502
|
+
}
|
|
503
|
+
for (const name of others) {
|
|
504
|
+
if (upstreamEnabled && MATT_DEP_SET.has(name) && upstreamHandled.has(name)) {
|
|
505
|
+
console.log(
|
|
506
|
+
(opts.dryRun
|
|
507
|
+
? " [dry-run] 将由上游安装完整版,跳过轻量版:"
|
|
508
|
+
: " 已由上游安装完整版,跳过轻量版:") + name
|
|
509
|
+
);
|
|
510
|
+
record(name, { kind: "upstream", action: "installed", agent: opts.mattAgent });
|
|
511
|
+
continue;
|
|
512
|
+
}
|
|
513
|
+
const dest = path.join(target, name);
|
|
514
|
+
if (isDir(dest) || isFile(dest)) {
|
|
515
|
+
console.log(" 同名 skill 已存在,保留用户版本:" + name);
|
|
516
|
+
record(name, { kind: depKind(name), action: "skipped" });
|
|
517
|
+
continue;
|
|
518
|
+
}
|
|
519
|
+
copySkill(name, dest, opts.dryRun);
|
|
520
|
+
record(name, { kind: depKind(name), action: "installed" });
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
if (opts.dryRun) {
|
|
524
|
+
console.log("dry-run 完成,未写入任何文件。");
|
|
525
|
+
} else {
|
|
526
|
+
const prev = loadManifest(target);
|
|
527
|
+
const merged = mergeEntries(prev ? prev.entries : {}, freshEntries);
|
|
528
|
+
writeManifest(target, merged);
|
|
529
|
+
console.log("安装完成。");
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
function confirmUninstall(opts) {
|
|
534
|
+
// 返回 "yes"|"no"|"refuse"。--force 直接视为 yes。
|
|
535
|
+
if (opts.force) {
|
|
536
|
+
return "yes";
|
|
537
|
+
}
|
|
538
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
539
|
+
return "refuse";
|
|
540
|
+
}
|
|
541
|
+
process.stdout.write("确认卸载本包安装的 skill?[y/N] ");
|
|
542
|
+
let answer = "";
|
|
543
|
+
try {
|
|
544
|
+
answer = readLineSync().trim().toLowerCase();
|
|
545
|
+
} catch (e) {
|
|
546
|
+
answer = "";
|
|
547
|
+
}
|
|
548
|
+
return answer === "y" || answer === "yes" ? "yes" : "no";
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
function runUninstall(opts) {
|
|
552
|
+
const target = opts.target;
|
|
553
|
+
const manifest = loadManifest(target);
|
|
554
|
+
|
|
555
|
+
// 无清单:尽力而为,仅处理主 skill。
|
|
556
|
+
if (!manifest) {
|
|
557
|
+
console.log("⚠ 未找到安装清单:" + manifestPath(target) + "(无清单,尽力而为)");
|
|
558
|
+
const mainDest = path.join(target, MAIN_SKILL);
|
|
559
|
+
if (!existsAt(mainDest)) {
|
|
560
|
+
console.log("目标目录中不存在 " + MAIN_SKILL + ",无需卸载。");
|
|
561
|
+
process.exit(0);
|
|
562
|
+
}
|
|
563
|
+
console.log("仅处理主 skill:" + MAIN_SKILL + "(其余 skill 保持不动)");
|
|
564
|
+
if (opts.dryRun) {
|
|
565
|
+
console.log(" [dry-run] 将删除 " + MAIN_SKILL + " → " + mainDest);
|
|
566
|
+
console.log("dry-run 完成,未写入任何文件。");
|
|
567
|
+
process.exit(0);
|
|
568
|
+
}
|
|
569
|
+
const decision = confirmUninstall(opts);
|
|
570
|
+
if (decision === "refuse") {
|
|
571
|
+
console.log("非交互式终端:为避免误删,已拒绝卸载。如确认请显式传入 --force。");
|
|
572
|
+
process.exit(1);
|
|
573
|
+
}
|
|
574
|
+
if (decision === "no") {
|
|
575
|
+
console.log("已取消卸载。");
|
|
576
|
+
process.exit(0);
|
|
577
|
+
}
|
|
578
|
+
fs.rmSync(mainDest, { recursive: true, force: true });
|
|
579
|
+
console.log(" ✓ 已删除 " + MAIN_SKILL);
|
|
580
|
+
console.log("清单缺失:其余 skill 一律保持不动。");
|
|
581
|
+
process.exit(0);
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// 依据清单归属构建计划:record 说「本包安装」才删,skipped 永不删。
|
|
585
|
+
const entries = manifest.entries || {};
|
|
586
|
+
const removeLocal = [];
|
|
587
|
+
const removeUpstream = [];
|
|
588
|
+
const keep = [];
|
|
589
|
+
for (const name of Object.keys(entries).sort()) {
|
|
590
|
+
const e = entries[name] || {};
|
|
591
|
+
if (e.action === "installed" || e.action === "forced") {
|
|
592
|
+
if (e.kind === "upstream") {
|
|
593
|
+
removeUpstream.push({ name, agent: e.agent || UPSTREAM_DEFAULT_AGENT });
|
|
594
|
+
} else {
|
|
595
|
+
removeLocal.push(name);
|
|
596
|
+
}
|
|
597
|
+
} else {
|
|
598
|
+
// skipped 或未知状态:一律保留。
|
|
599
|
+
keep.push(name);
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
console.log("目标目录:" + target + (opts.dryRun ? "(dry-run,不会写入)" : ""));
|
|
604
|
+
const allRemove = removeLocal.concat(removeUpstream.map((u) => u.name));
|
|
605
|
+
console.log("将移除(本包安装):" + (allRemove.length ? allRemove.join("、") : "(无)"));
|
|
606
|
+
for (const name of keep) {
|
|
607
|
+
console.log("保留(安装时已存在,非本包安装):" + name);
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
if (allRemove.length === 0) {
|
|
611
|
+
console.log("没有需要移除的 skill(清单中无本包安装项)。");
|
|
612
|
+
process.exit(0);
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
if (opts.dryRun) {
|
|
616
|
+
for (const name of removeLocal) {
|
|
617
|
+
console.log(" [dry-run] 将删除 " + name + " → " + path.join(target, name));
|
|
618
|
+
}
|
|
619
|
+
for (const [agent, names] of groupByAgent(removeUpstream)) {
|
|
620
|
+
console.log(" [dry-run] 上游完整版(" + agent + "):" + upstreamRemoveString(names, agent));
|
|
621
|
+
}
|
|
622
|
+
console.log(" [dry-run] 将删除安装清单:" + manifestPath(target));
|
|
623
|
+
console.log("dry-run 完成,未写入任何文件。");
|
|
624
|
+
process.exit(0);
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
const decision = confirmUninstall(opts);
|
|
628
|
+
if (decision === "refuse") {
|
|
629
|
+
console.log("非交互式终端:为避免误删,已拒绝卸载。如确认请显式传入 --force。");
|
|
630
|
+
process.exit(1);
|
|
631
|
+
}
|
|
632
|
+
if (decision === "no") {
|
|
633
|
+
console.log("已取消卸载。");
|
|
634
|
+
process.exit(0);
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
for (const name of removeLocal) {
|
|
638
|
+
fs.rmSync(path.join(target, name), { recursive: true, force: true });
|
|
639
|
+
console.log(" ✓ 已删除 " + name);
|
|
640
|
+
}
|
|
641
|
+
for (const [agent, names] of groupByAgent(removeUpstream)) {
|
|
642
|
+
console.log("上游完整版(" + agent + "):");
|
|
643
|
+
removeUpstreamWithFallback(names, agent, target);
|
|
644
|
+
}
|
|
645
|
+
removeManifest(target);
|
|
646
|
+
console.log("卸载完成。");
|
|
647
|
+
process.exit(0);
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
function runCheck(opts) {
|
|
651
|
+
const target = opts.target;
|
|
652
|
+
console.log("检查依赖 skill 解析状态(目标目录:" + target + "):");
|
|
653
|
+
|
|
654
|
+
let ok = true;
|
|
655
|
+
for (const name of REF_NAMES) {
|
|
656
|
+
const installed = isFile(path.join(target, name, "SKILL.md"));
|
|
657
|
+
if (installed) {
|
|
658
|
+
console.log(" " + name + ":按名可解析(skill 已安装)");
|
|
659
|
+
continue;
|
|
660
|
+
}
|
|
661
|
+
const fallbacks = REF_FALLBACKS[name] || [];
|
|
662
|
+
const hit = fallbacks.find((file) => isFile(path.join(DEPS_DIR, file)));
|
|
663
|
+
if (hit) {
|
|
664
|
+
console.log(" " + name + ":ref 兜底可用(references/deps/" + hit + ")");
|
|
665
|
+
continue;
|
|
666
|
+
}
|
|
667
|
+
ok = false;
|
|
668
|
+
console.log(
|
|
669
|
+
" " + name + ":无法解析(未安装,且缺少 ref 兜底:" +
|
|
670
|
+
fallbacks.map((f) => "references/deps/" + f).join("、") +
|
|
671
|
+
")"
|
|
672
|
+
);
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
if (ok) {
|
|
676
|
+
console.log("全部依赖 skill 均已解析。");
|
|
677
|
+
process.exit(0);
|
|
678
|
+
} else {
|
|
679
|
+
console.log("存在无法解析的依赖 skill。");
|
|
680
|
+
process.exit(1);
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
function main() {
|
|
685
|
+
const opts = parseArgs(process.argv.slice(2));
|
|
686
|
+
if (opts.command === "check") {
|
|
687
|
+
runCheck(opts);
|
|
688
|
+
} else if (opts.command === "uninstall") {
|
|
689
|
+
runUninstall(opts);
|
|
690
|
+
} else {
|
|
691
|
+
runInstall(opts);
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
main();
|