@heihei0299/matt-skills 3.0.12 → 3.0.14
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 +9 -2
- package/bin/cli.js +97 -9
- package/package.json +1 -1
- package/template/AGENTS.md +0 -2
package/README.md
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
面向项目仓库的 Agent skills 与配置模板。模板包含项目 skeleton、`AGENTS.md`、项目上下文占位文件,以及 pi / opencode 所需的项目配置;共享 Skills 由 CLI 从 Workspace 的 canonical source 直接组装到目标项目。
|
|
4
4
|
|
|
5
|
+
## 环境要求
|
|
6
|
+
|
|
7
|
+
- Node.js `>=18`。
|
|
8
|
+
- npm(随 Node.js 提供,用于安装和运行 CLI;也可通过 `npx` 直接调用)。
|
|
9
|
+
- Git(`sync`、`check` 和发布流程需要)。
|
|
10
|
+
|
|
5
11
|
## 模板内容
|
|
6
12
|
|
|
7
13
|
```text
|
|
@@ -15,6 +21,7 @@ template/
|
|
|
15
21
|
共享 Skills 不作为 Template Snapshot 的持久化副本;`init`、普通 `sync` 和项目级 `install` 都从 Workspace 的 canonical source 分发到唯一的 `.agents/skills/`。`.pi/skills/`、`.opencode/skills/` 和 `.claude/skills/` 仅用于项目自定义 skills。
|
|
16
22
|
|
|
17
23
|
- `PROJECT.md` 描述项目是什么;操作规则放在 `AGENTS.md`。
|
|
24
|
+
- 分发的 `AGENTS.md` 提供通用工作流;项目专用规则、命令、测试方式和完成标准由目标项目更具体的 `AGENTS.md` 补充。
|
|
18
25
|
- `.opencode/CONTEXT.md` / `.pi/CONTEXT.md` 保存领域术语与边界。
|
|
19
26
|
- `.agents/skills/` 承载共享 skills;同步只处理可分发 skill 名称,不删除额外的项目自定义 skills。
|
|
20
27
|
- `ci-guard`、`commit-check` 是本仓库维护用的 repo-local skills,不会分发到目标项目。
|
|
@@ -55,7 +62,7 @@ npx @heihei0299/matt-skills sync --all # 同步全部可分发 skills
|
|
|
55
62
|
npx @heihei0299/matt-skills sync --dry-run --json
|
|
56
63
|
```
|
|
57
64
|
|
|
58
|
-
`init` 默认保护已有 `AGENTS.md`;需要刷新完整模板时使用 `init --all`。默认 `sync`
|
|
65
|
+
`init` 默认保护已有 `AGENTS.md`;需要刷新完整模板时使用 `init --all`。默认 `sync` 保留已有 `AGENTS.md` 和项目规则,`sync --all` 才将其整体刷新为当前分发模板。`sync` 不删除目标项目的额外文件或自定义 skills。
|
|
59
66
|
|
|
60
67
|
默认 programming 范围中的 4 个独有 skills 是 `tdd-implement`、`diagnose-fix`、`grill-to-spec`、`show-me`。
|
|
61
68
|
|
|
@@ -100,7 +107,7 @@ npx @heihei0299/matt-skills sync --all # 同步全部可分发范围
|
|
|
100
107
|
|
|
101
108
|
## 发布
|
|
102
109
|
|
|
103
|
-
|
|
110
|
+
版本由推送的 `v*` 标签决定;GitHub Actions 会在发布时从 tag 设置 npm 包版本,并执行全量测试与模板检查。
|
|
104
111
|
|
|
105
112
|
```sh
|
|
106
113
|
npm test
|
package/bin/cli.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { readdir, readFile, writeFile, cp, stat, rm, mkdir } from 'node:fs/promises';
|
|
2
|
+
import { readdir, readFile, writeFile, cp, stat, lstat, rm, mkdir } from 'node:fs/promises';
|
|
3
3
|
import os from 'node:os';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
@@ -74,7 +74,7 @@ Sync options:
|
|
|
74
74
|
--help, -h Show this help
|
|
75
75
|
项目 skills:${PROJECT_SKILL_DIRS}
|
|
76
76
|
|
|
77
|
-
说明:默认不带 --all 增量同步默认 programming skill
|
|
77
|
+
说明:默认不带 --all 增量同步默认 programming skill,并保留现有 AGENTS.md;--all 时同步全部可分发 skill 并强制刷新 AGENTS.md。
|
|
78
78
|
|
|
79
79
|
提示:matt-skills --help 查看全量
|
|
80
80
|
`;
|
|
@@ -236,6 +236,57 @@ function projectSkillTargets(target) {
|
|
|
236
236
|
return [...targets.values()];
|
|
237
237
|
}
|
|
238
238
|
|
|
239
|
+
const LEGACY_PROJECT_SKILL_DIRS = ['.pi/skills', '.opencode/skills', '.claude/skills'];
|
|
240
|
+
|
|
241
|
+
async function isSafeRealPath(targetPath) {
|
|
242
|
+
const absolute = path.resolve(targetPath);
|
|
243
|
+
const root = path.parse(absolute).root;
|
|
244
|
+
let current = root;
|
|
245
|
+
for (const segment of path.relative(root, absolute).split(path.sep).filter(Boolean)) {
|
|
246
|
+
current = path.join(current, segment);
|
|
247
|
+
try {
|
|
248
|
+
if ((await lstat(current)).isSymbolicLink()) return false;
|
|
249
|
+
} catch {
|
|
250
|
+
return false;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
return true;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function sameMetadata(left, right) {
|
|
257
|
+
return (left.mode & 0o7777) === (right.mode & 0o7777)
|
|
258
|
+
&& left.uid === right.uid
|
|
259
|
+
&& left.gid === right.gid;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
async function sameTree(left, right) {
|
|
263
|
+
try {
|
|
264
|
+
const [leftInfo, rightInfo] = await Promise.all([lstat(left), lstat(right)]);
|
|
265
|
+
if (leftInfo.isSymbolicLink() || rightInfo.isSymbolicLink()) return false;
|
|
266
|
+
if (leftInfo.isDirectory() !== rightInfo.isDirectory()) return false;
|
|
267
|
+
if (leftInfo.isDirectory()) {
|
|
268
|
+
const [leftEntries, rightEntries] = await Promise.all([
|
|
269
|
+
readdir(left, { withFileTypes: true }),
|
|
270
|
+
readdir(right, { withFileTypes: true }),
|
|
271
|
+
]);
|
|
272
|
+
if (!sameMetadata(leftInfo, rightInfo) || leftEntries.length !== rightEntries.length) return false;
|
|
273
|
+
const rightNames = new Set(rightEntries.map((entry) => entry.name));
|
|
274
|
+
for (const entry of leftEntries) {
|
|
275
|
+
if (!rightNames.has(entry.name) || !await sameTree(path.join(left, entry.name), path.join(right, entry.name))) {
|
|
276
|
+
return false;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
return true;
|
|
280
|
+
}
|
|
281
|
+
if (leftInfo.isFile() && rightInfo.isFile()) {
|
|
282
|
+
return (await readFile(left)).equals(await readFile(right)) && sameMetadata(leftInfo, rightInfo);
|
|
283
|
+
}
|
|
284
|
+
return false;
|
|
285
|
+
} catch {
|
|
286
|
+
return false;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
239
290
|
const GLOBAL_DIRS = {
|
|
240
291
|
codex: '.codex/skills',
|
|
241
292
|
pi: '.pi/agent/skills',
|
|
@@ -441,12 +492,20 @@ async function syncCommand({ dest, all, dryRun, json, upstreamUrl, ref }) {
|
|
|
441
492
|
const allSkills = await listSkillNames({ onlyProgramming });
|
|
442
493
|
const skillTargets = projectSkillTargets(target);
|
|
443
494
|
const skillsDir = path.resolve(target, PROJECT_DIRS.codex);
|
|
444
|
-
const
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
isWorkspaceSource: path.resolve(dir) === path.resolve(SKILLS_DIR),
|
|
495
|
+
const legacyLocations = LEGACY_PROJECT_SKILL_DIRS.map((relative) => ({
|
|
496
|
+
dir: path.resolve(target, relative),
|
|
497
|
+
label: relative,
|
|
498
|
+
isWorkspaceSource: false,
|
|
449
499
|
}));
|
|
500
|
+
const preservedRepoLocal = [];
|
|
501
|
+
const preserveLocations = [
|
|
502
|
+
...skillTargets.map(({ tool, dir }) => ({
|
|
503
|
+
dir,
|
|
504
|
+
label: PROJECT_DIRS[tool],
|
|
505
|
+
isWorkspaceSource: path.resolve(dir) === path.resolve(SKILLS_DIR),
|
|
506
|
+
})),
|
|
507
|
+
...legacyLocations,
|
|
508
|
+
];
|
|
450
509
|
for (const { dir } of skillTargets) await mkdir(dir, { recursive: true });
|
|
451
510
|
for (const name of REPO_LOCAL_SKILLS) {
|
|
452
511
|
for (const location of preserveLocations) {
|
|
@@ -476,8 +535,34 @@ async function syncCommand({ dest, all, dryRun, json, upstreamUrl, ref }) {
|
|
|
476
535
|
}
|
|
477
536
|
}
|
|
478
537
|
}
|
|
479
|
-
|
|
480
|
-
|
|
538
|
+
const distributableSkillNames = (await listSkillNames({ onlyProgramming: false })).filter((name) => !REPO_LOCAL_SKILLS.has(name));
|
|
539
|
+
const cleanedLegacy = [];
|
|
540
|
+
for (const location of legacyLocations) {
|
|
541
|
+
let entries;
|
|
542
|
+
try {
|
|
543
|
+
if (!await isSafeRealPath(location.dir)) continue;
|
|
544
|
+
const locationInfo = await lstat(location.dir);
|
|
545
|
+
if (!locationInfo.isDirectory()) continue;
|
|
546
|
+
entries = await readdir(location.dir, { withFileTypes: true });
|
|
547
|
+
} catch {
|
|
548
|
+
continue;
|
|
549
|
+
}
|
|
550
|
+
for (const entry of entries) {
|
|
551
|
+
if (!entry.isDirectory() || entry.name === '.git' || entry.name.endsWith('.bak')) continue;
|
|
552
|
+
if (!distributableSkillNames.includes(entry.name)) continue;
|
|
553
|
+
const src = path.join(SKILLS_DIR, entry.name);
|
|
554
|
+
const dst = path.join(location.dir, entry.name);
|
|
555
|
+
try {
|
|
556
|
+
if (await sameTree(src, dst)) {
|
|
557
|
+
await rm(dst, { recursive: true, force: true });
|
|
558
|
+
cleanedLegacy.push(`${location.label}/${entry.name}`);
|
|
559
|
+
}
|
|
560
|
+
} catch {}
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
// Legacy harness dirs may contain project-local Skills. Only exact canonical
|
|
564
|
+
// trees of clearly distributable skills are removed; repo-local, modified or
|
|
565
|
+
// unknown trees remain untouched.
|
|
481
566
|
// 清理过时的 .pi/settings.json 指向
|
|
482
567
|
try {
|
|
483
568
|
const piSettings = path.join(target, '.pi/settings.json');
|
|
@@ -489,6 +574,9 @@ async function syncCommand({ dest, all, dryRun, json, upstreamUrl, ref }) {
|
|
|
489
574
|
}
|
|
490
575
|
}
|
|
491
576
|
} catch {}
|
|
577
|
+
if (cleanedLegacy.length) {
|
|
578
|
+
process.stdout.write(`迁移清理:${cleanedLegacy.join(', ')} 旧共享副本已移除\n`);
|
|
579
|
+
}
|
|
492
580
|
if (preservedRepoLocal.length) {
|
|
493
581
|
process.stdout.write(`迁移提示:${preservedRepoLocal.join(', ')} 已不再分发,现有副本已保留\n`);
|
|
494
582
|
}
|
package/package.json
CHANGED
package/template/AGENTS.md
CHANGED