@heihei0299/matt-skills 3.0.13 → 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 +1 -3
- package/bin/cli.js +96 -8
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,8 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
面向项目仓库的 Agent skills 与配置模板。模板包含项目 skeleton、`AGENTS.md`、项目上下文占位文件,以及 pi / opencode 所需的项目配置;共享 Skills 由 CLI 从 Workspace 的 canonical source 直接组装到目标项目。
|
|
4
4
|
|
|
5
|
-
当前发布版本:`v3.0.12`。
|
|
6
|
-
|
|
7
5
|
## 环境要求
|
|
8
6
|
|
|
9
7
|
- Node.js `>=18`。
|
|
@@ -109,7 +107,7 @@ npx @heihei0299/matt-skills sync --all # 同步全部可分发范围
|
|
|
109
107
|
|
|
110
108
|
## 发布
|
|
111
109
|
|
|
112
|
-
|
|
110
|
+
版本由推送的 `v*` 标签决定;GitHub Actions 会在发布时从 tag 设置 npm 包版本,并执行全量测试与模板检查。
|
|
113
111
|
|
|
114
112
|
```sh
|
|
115
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';
|
|
@@ -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
|
}
|