@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 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` 只刷新 `AGENTS.md` 的 `matt-skills:managed` 受管区块并保留标记外的项目规则;旧的无标记 `AGENTS.md` 原样保留,`sync --all` 才整体刷新。`sync` 不删除目标项目的额外文件或自定义 skills。
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
- 推送 `v*` 标签会触发 GitHub Actions:全量测试、模板检查和 npm 发布。
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,并只更新 AGENTS.md 的 matt-skills 受管区块;没有受管标记的现有 AGENTS.md 原样保留。--all 时同步全部可分发 skill 并强制刷新 AGENTS.md。
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 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.12",
3
+ "version": "3.0.14",
4
4
  "description": "Agent skills + 项目配置模板:一条命令初始化 opencode / pi-agent 项目(含 mattpocock/skills 上游技能)",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,6 +1,5 @@
1
1
  # AGENTS.md
2
2
 
3
- <!-- matt-skills:managed:start -->
4
3
  ## Workflow
5
4
 
6
5
  按任务选择最匹配的 skill / 工具:
@@ -76,4 +75,3 @@ codegraph node "<符号>"
76
75
  ## Completion
77
76
 
78
77
  完成时说明改动或审查结论、已执行验证、未执行验证及原因、剩余风险;如有 commit,报告 commit hash。
79
- <!-- matt-skills:managed:end -->