@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.
Files changed (3) hide show
  1. package/README.md +1 -3
  2. package/bin/cli.js +96 -8
  3. 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
- 最近发布:`v3.0.12`。推送 `v*` 标签会触发 GitHub Actions:全量测试、模板检查和 npm 发布。
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 preservedRepoLocal = [];
445
- const preserveLocations = skillTargets.map(({ tool, dir }) => ({
446
- dir,
447
- label: PROJECT_DIRS[tool],
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
- // Project skill directories may contain project-local Skills. Preserve them because
480
- // their origin cannot be distinguished safely from a historical shared mirror.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@heihei0299/matt-skills",
3
- "version": "3.0.13",
3
+ "version": "3.0.14",
4
4
  "description": "Agent skills + 项目配置模板:一条命令初始化 opencode / pi-agent 项目(含 mattpocock/skills 上游技能)",
5
5
  "type": "module",
6
6
  "bin": {