@heihei0299/matt-skills 1.3.0 → 1.3.1

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
@@ -36,11 +36,13 @@ npx @heihei0299/matt-skills init
36
36
  **增量同步(已有项目)**:已有项目更新到最新模板与技能:
37
37
 
38
38
  ```sh
39
- npx @heihei0299/matt-skills sync # 增量同步,直接更新
40
- npx @heihei0299/matt-skills sync --force # 同上(直接覆盖)
39
+ npx @heihei0299/matt-skills sync # 默认仅对比不写盘(check),有差异 exit 1,--json 可解析
40
+ npx @heihei0299/matt-skills sync --apply # 安全增量:AGENTS.md 定制跳过,上游技能强制覆盖但不删
41
+ npx @heihei0299/matt-skills sync --force # 硬盖:AGENTS.md 备份后硬盖,技能与模板全量 add/update/remove
42
+ npx @heihei0299/matt-skills sync --apply --dest <path> --upstream <url> --ref <ref> --json # 选项可组合
41
43
  ```
42
44
 
43
- `sync` 专为已有项目设计:自动检测 `AGENTS.md` 是否存在,存在则增量更新模板与上游技能(直接覆盖,`AGENTS.md` 会备份),不存在则等同全新 `init`。与 `init --force` 的区别:`sync` 语义更明确,建议已有项目优先用 `sync`。
45
+ `sync` 专为已有项目设计,三档语义:默认 `check` 仅对比不写盘(打印“上游 HEAD / 本地非独有 vs 上游 / 新增/更新/删除/一致”表,`--json` 可解析,有差异 `exit 1`);`--apply` 安全增量写盘(`AGENTS.md` 若含独有路由如 `tdd-implement` 则跳过,`.agents/skills` 对上游技能 `rm+cp force` 覆盖但不 `remove`,`template/.opencode/.pi` 增量 `add/update`);`--force` 硬盖(`AGENTS.md` 备份 `.bak` 后强制覆盖,技能与模板均 `add/update/remove` 全做)。`--dest`、`--upstream`、`--ref`、`--json` 在三档均可透传。
44
46
 
45
47
  上游没有 `tdd-implement`、`grill-to-spec`、`diagnose-fix`、`commit-check`,复制天然不冲突。目标仓库会话即自动加载全部技能(上游在 `.agents/skills/`、独有在 `.opencode/skills/`;pi 侧独有在 `.pi/skills/`)与项目级全局配置(行为路由表、分文件约定);`issue-audit` 以子代理 + 命令形式分发(`.opencode/agents/`、`.opencode/commands/`);9 个显式触发技能注册为 opencode 命令(`.opencode/commands/`,`/命令名` 触发)。
46
48
 
@@ -115,14 +117,16 @@ pi 下对应能力以内置工具或已装扩展为准(`AGENTS.md`「能力边
115
117
  仓库内提供安装管理 CLI(`bin/cli.js`,依赖 `prompts`,见 `package.json`),同时作为 npm 包 `@heihei0299/matt-skills` 分发(`npx @heihei0299/matt-skills <command>`):
116
118
 
117
119
  ```sh
118
- node bin/cli.js init [--dest <dir>] [--force] # 初始化项目:template + 上游技能
119
- node bin/cli.js sync [--dest <dir>] [--force] # 同步已有项目到最新(直接覆盖)
120
- node bin/cli.js list [--json] # 列出 .agents/skills/ 下全部技能及描述
121
- node bin/cli.js install [选项] # 把技能复制到目标工具目录(交互式选择)
120
+ node bin/cli.js init [--dest <dir>] [--force] # 初始化项目:template + 上游技能
121
+ node bin/cli.js sync [--apply|--force] [--dest <path>] [--upstream <url>] [--ref <ref>] [--json] # 同步已有项目到最新
122
+ node bin/cli.js list [--json] # 列出 .agents/skills/ 下全部技能及描述
123
+ node bin/cli.js install [选项] # 把技能复制到目标工具目录(交互式选择)
124
+ node bin/cli.js check [--json] [--upstream <url>] [--ref <ref>] # 只读检查上游技能是否最新(等价 sync 默认)
122
125
  ```
123
126
 
124
- `init` 选项:`--dest <dir>` 指定目标目录(默认当前目录);`--force` 覆盖已存在的文件(直接覆盖,`AGENTS.md` 会备份到 `.bak`)(默认跳过),见「初始化」。
125
- `sync` 选项:`--dest <dir>` 指定目标目录;`--force` 直接覆盖(与默认一致),见「初始化」增量同步。
127
+ `init` 选项:`--dest <path>` 指定目标目录(默认当前目录);`--force` 覆盖已存在的文件(直接覆盖,`AGENTS.md` 会备份到 `.bak`)(默认跳过),见「初始化」。
128
+ `sync` 选项:`--apply` 安全增量(`AGENTS.md` 定制跳过、技能不删),`--force` 硬盖(`AGENTS.md` 备份后硬盖、技能与模板全量 `remove`);`--dest <path>` 目标目录;`--upstream <url>` 上游地址;`--ref <ref>` 上游分支;`--json` JSON 输出;默认无参等价 `check` 仅对比不写盘,`--json` 可解析,有差异 `exit 1`。
129
+ `check` 选项:`--json`、`--upstream <url>`、`--ref <ref>`(等价 `sync` 默认 `check`)。
126
130
 
127
131
  `install` 选项:
128
132
 
@@ -136,15 +140,15 @@ node bin/cli.js install [选项] # 把技能复制到目标工
136
140
 
137
141
  本仓库的 `.agents/skills/` 中 **非独有技能** 来自 `mattpocock/skills` 上游。已实现双通道自动同步:
138
142
 
139
- - **本地 CLI**:`matt-skills check`(只读比对)与 `matt-skills update`(一键覆盖本地 `.agents/skills/`,自动处理 `writing-great-skills writing-for-agents` 重命名与增删)
143
+ - **本地 CLI**:`matt-skills sync` 三档——默认 `check` 只读比对(等价 `matt-skills check`,有差异 `exit 1`,`--json` 可解析)、`matt-skills sync --apply` 安全增量、`matt-skills sync --force` 硬盖;`matt-skills check [--json] [--upstream <url>] [--ref <ref>]` 仍保留为只读别名;`matt-skills update` 已合并到 `sync --apply`(执行提示 `update 已合并到 sync --apply` 且 `exit 1`)
140
144
  - **GitHub Actions**:`.github/workflows/sync-upstream.yml` 每周一 02:00 UTC 自动 `check`,有差异则 `apply` 并提 PR(`upstream-sync/<short-sha>`),支持 `workflow_dispatch` 手动触发(`ref`/`dry_run` 参数)
141
145
 
142
146
  ```sh
143
- npx @heihei0299/matt-skills check # 只读检查,对比本地 vs 上游 HEAD(有差异 exit 1)
144
- npx @heihei0299/matt-skills check --json # JSON 输出:{ head, counts, result: { added, updated, renamed, removed, same } }
145
- npx @heihei0299/matt-skills update --dry-run # 演练,不写文件
146
- npx @heihei0299/matt-skills update # 覆盖 .agents/skills 非独有技能
147
- node scripts/sync-upstream.js --check # 等价底层脚本(CLI check/update 的实现)
147
+ npx @heihei0299/matt-skills sync --json # 默认 check 只读检查,JSON 输出:{ head, counts, result: { added, updated, renamed, removed, same } }
148
+ npx @heihei0299/matt-skills sync --apply # 安全增量写盘
149
+ npx @heihei0299/matt-skills sync --force # 硬盖写盘
150
+ npx @heihei0299/matt-skills check --json # 等价 sync 默认 check
151
+ node scripts/sync-upstream.js --check # 等价底层脚本(CLI sync/check 的实现)
148
152
  node scripts/sync-upstream.js --apply --dry-run
149
153
  ```
150
154
 
package/bin/cli.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { readdir, readFile, cp, stat } from 'node:fs/promises';
2
+ import { readdir, readFile, cp, stat, 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';
@@ -17,31 +17,27 @@ const HELP = `matt-skills — install and manage this skill collection
17
17
 
18
18
  Usage:
19
19
  matt-skills init [options] Initialize a project: template + upstream skills
20
- matt-skills sync [options] Sync existing project to latest template + skills
20
+ matt-skills sync [--apply|--force] [--dest <path>] [--upstream <url>] [--ref <ref>] [--json] Sync existing project to latest template + skills
21
21
  matt-skills list [--json] List available skills and their descriptions
22
22
  matt-skills install [options] Install skills (interactive by default)
23
23
  matt-skills check [--json] [--upstream <url>] [--ref <ref>]
24
24
  Check if upstream skills are up to date (read-only)
25
- matt-skills update [--dry-run] [--force] [--upstream <url>] [--ref <ref>]
26
- Update .agents/skills from upstream (auto handles renames)
27
25
  matt-skills --help Show this help
28
26
 
29
27
  Init options:
30
28
  --dest <path> Target directory (default: current directory)
31
29
  --force Overwrite existing files
32
30
  Sync options:
31
+ --apply Apply changes (safe incremental, respects custom AGENTS.md)
32
+ --force Hard overwrite (backup .bak + full sync)
33
33
  --dest <path> Target directory (default: current directory)
34
- --force Overwrite existing files
34
+ --upstream <url> Upstream repo URL (default: https://github.com/mattpocock/skills.git)
35
+ --ref <ref> Upstream ref (default: HEAD)
36
+ --json Output as JSON
35
37
  Check options:
36
38
  --json Output as JSON
37
39
  --upstream <url> Upstream repo URL (default: https://github.com/mattpocock/skills.git)
38
40
  --ref <ref> Upstream ref (default: HEAD)
39
- Update options:
40
- --dry-run Show what would change without writing files
41
- --force Force overwrite (default is to overwrite; kept for compatibility)
42
- --upstream <url> Upstream repo URL
43
- --ref <ref> Upstream ref
44
-
45
41
  Install options:
46
42
  --tools <a,b> Install for the given tools (codex, pi, opencode, claude); skips tool selection
47
43
  --all Install all skills; skips skill selection
@@ -224,22 +220,69 @@ async function initCommand({ dest, force }) {
224
220
  process.stdout.write(`目标路径:${target}\n`);
225
221
  }
226
222
 
227
- async function syncCommand({ dest, force }) {
223
+ async function syncCommand({ dest, force, apply, upstreamUrl, ref, json }) {
224
+ // 默认:仅对比不写盘 (check 模式),--apply / --force 显式写盘
225
+ const doApply = apply || force;
226
+ if (!doApply) {
227
+ const { compare } = await import('../scripts/sync-upstream.js');
228
+ const cmp = await compare({ upstreamUrl, ref });
229
+ if (json) {
230
+ process.stdout.write(JSON.stringify({ head: cmp.head, counts: cmp.counts, result: cmp.result }, null, 2) + '\n');
231
+ } else {
232
+ const lines = [];
233
+ lines.push(`上游 HEAD: ${cmp.head}`);
234
+ lines.push(`本地非独有: ${cmp.counts.local} 上游: ${cmp.counts.upstream}`);
235
+ lines.push('');
236
+ const totalDiff = cmp.result.added.length + cmp.result.updated.length + cmp.result.removed.length + cmp.result.renamed.length;
237
+ if (totalDiff === 0) {
238
+ lines.push('✅ 已是最新,无差异');
239
+ } else {
240
+ if (cmp.result.added.length) lines.push(`新增 (${cmp.result.added.length}): ${cmp.result.added.join(', ')}`);
241
+ if (cmp.result.updated.length) lines.push(`更新 (${cmp.result.updated.length}): ${cmp.result.updated.join(', ')}`);
242
+ if (cmp.result.renamed.length) lines.push(`重命名 (${cmp.result.renamed.length}): ${cmp.result.renamed.map((r) => `${r.from}→${r.to}`).join(', ')}`);
243
+ if (cmp.result.removed.length) lines.push(`删除 (${cmp.result.removed.length}): ${cmp.result.removed.join(', ')}`);
244
+ if (cmp.result.same.length) lines.push(`一致 (${cmp.result.same.length}): ${cmp.result.same.join(', ')}`);
245
+ }
246
+ process.stdout.write(lines.join('\n') + '\n');
247
+ }
248
+ const { rm } = await import('node:fs/promises');
249
+ await rm(cmp.dest, { recursive: true, force: true });
250
+ const hasDiff = cmp.result.added.length + cmp.result.updated.length + cmp.result.removed.length + cmp.result.renamed.length > 0;
251
+ if (hasDiff) process.exitCode = 1;
252
+ return;
253
+ }
254
+
228
255
  const target = dest ? path.resolve(process.cwd(), dest) : process.cwd();
229
256
  const marker = path.join(target, 'AGENTS.md');
230
- const backup = !force;
231
257
  if (!(await pathExists(marker))) {
232
258
  process.stdout.write('未检测到现有项目(AGENTS.md 不存在),将执行全新初始化\n');
233
259
  await cp(TEMPLATE_DIR, target, { recursive: true, force: true });
234
260
  process.stdout.write('模板:已复制(AGENTS.md、.opencode/、.pi/)\n');
235
- } else {
261
+ } else if (force) {
236
262
  process.stdout.write('同步:检测到现有项目,将增量更新\n');
237
- if (backup) {
238
- const cur = path.join(target, 'AGENTS.md');
239
- if (await pathExists(cur)) await backupIfExists(cur);
263
+ await backupIfExists(path.join(target, 'AGENTS.md'));
264
+ await cp(TEMPLATE_DIR, target, { recursive: true, force: true });
265
+ process.stdout.write('模板:已覆盖(AGENTS.md、.opencode/、.pi/)\n');
266
+ } else if (apply) {
267
+ process.stdout.write('同步:检测到现有项目,将增量更新\n');
268
+ let skipAgents = false;
269
+ try {
270
+ const content = await readFile(path.join(target, 'AGENTS.md'), 'utf8');
271
+ if (content.includes('tdd-implement')) skipAgents = true;
272
+ } catch {}
273
+ if (skipAgents) {
274
+ await cp(path.join(TEMPLATE_DIR, '.opencode'), path.join(target, '.opencode'), { recursive: true, force: true });
275
+ await cp(path.join(TEMPLATE_DIR, '.pi'), path.join(target, '.pi'), { recursive: true, force: true });
276
+ process.stdout.write('模板:已同步(AGENTS.md 跳过,已含定制)\n');
277
+ } else {
278
+ await cp(TEMPLATE_DIR, target, { recursive: true, force: true });
279
+ process.stdout.write('模板:已同步(AGENTS.md、.opencode/、.pi/)\n');
240
280
  }
281
+ } else {
282
+ // fallback for any other doApply case (should not reach here because !doApply already returned)
283
+ process.stdout.write('同步:检测到现有项目,将增量更新\n');
241
284
  await cp(TEMPLATE_DIR, target, { recursive: true, force: true });
242
- process.stdout.write(backup ? '模板:已同步(AGENTS.md、.opencode/、.pi/)\n' : '模板:已覆盖(AGENTS.md、.opencode/、.pi/)\n');
285
+ process.stdout.write('模板:已同步(AGENTS.md、.opencode/、.pi/)\n');
243
286
  }
244
287
  const entries = await readdir(SKILLS_DIR, { withFileTypes: true });
245
288
  const upstream = entries
@@ -247,6 +290,7 @@ async function syncCommand({ dest, force }) {
247
290
  .map((e) => e.name)
248
291
  .sort();
249
292
  const skillsDir = path.join(target, '.agents', 'skills');
293
+ await mkdir(skillsDir, { recursive: true });
250
294
  let installed = 0;
251
295
  let updated = 0;
252
296
  for (const name of upstream) {
@@ -258,6 +302,7 @@ async function syncCommand({ dest, force }) {
258
302
  }
259
303
  const exists = await pathExists(dst);
260
304
  if (exists) {
305
+ await rm(dst, { recursive: true, force: true });
261
306
  await cp(src, dst, { recursive: true, force: true });
262
307
  updated++;
263
308
  } else {
@@ -265,6 +310,20 @@ async function syncCommand({ dest, force }) {
265
310
  installed++;
266
311
  }
267
312
  }
313
+ if (force) {
314
+ let localEntries = [];
315
+ try {
316
+ localEntries = await readdir(skillsDir, { withFileTypes: true });
317
+ } catch {}
318
+ for (const e of localEntries) {
319
+ if (!e.isDirectory()) continue;
320
+ if (e.name.endsWith('.bak')) continue;
321
+ if (e.name === '.git') continue;
322
+ if (PROPRIETARY_SKILLS.has(e.name)) continue;
323
+ if (upstream.includes(e.name)) continue;
324
+ await rm(path.join(skillsDir, e.name), { recursive: true, force: true });
325
+ }
326
+ }
268
327
  process.stdout.write(`上游技能:新增 ${installed}、更新 ${updated}\n`);
269
328
  process.stdout.write(`目标路径:${target}\n`);
270
329
  }
@@ -272,13 +331,23 @@ async function syncCommand({ dest, force }) {
272
331
  function parseInitArgs(args) {
273
332
  let dest;
274
333
  let force = false;
334
+ let apply = false;
335
+ let json = false;
336
+ let upstreamUrl;
337
+ let ref;
275
338
  for (let i = 0; i < args.length; i++) {
276
339
  const arg = args[i];
277
340
  if (arg === '--dest') dest = args[++i];
278
341
  else if (arg.startsWith('--dest=')) dest = arg.slice('--dest='.length);
279
342
  else if (arg === '--force') force = true;
343
+ else if (arg === '--apply') apply = true;
344
+ else if (arg === '--json') json = true;
345
+ else if (arg === '--upstream') upstreamUrl = args[++i];
346
+ else if (arg.startsWith('--upstream=')) upstreamUrl = arg.slice('--upstream='.length);
347
+ else if (arg === '--ref') ref = args[++i];
348
+ else if (arg.startsWith('--ref=')) ref = arg.slice('--ref='.length);
280
349
  }
281
- return { dest, force };
350
+ return { dest, force, apply, json, upstreamUrl, ref };
282
351
  }
283
352
 
284
353
  function parseInstallArgs(args) {
@@ -337,44 +406,6 @@ async function checkCommand(args) {
337
406
  if (hasDiff) process.exitCode = 1;
338
407
  }
339
408
 
340
- async function updateCommand(args) {
341
- const { applySync } = await import('../scripts/sync-upstream.js');
342
- const dryRun = args.includes('--dry-run');
343
- const json = args.includes('--json');
344
- const upstreamIdx = args.indexOf('--upstream');
345
- const upstreamUrl = upstreamIdx !== -1 ? args[upstreamIdx + 1] : undefined;
346
- const refIdx = args.indexOf('--ref');
347
- const ref = refIdx !== -1 ? args[refIdx + 1] : undefined;
348
- const res = await applySync({ upstreamUrl, ref, dryRun });
349
- if (json) {
350
- process.stdout.write(JSON.stringify({ head: res.head, result: res.result, actions: res.actions, dryRun }, null, 2) + '\n');
351
- } else {
352
- const lines = [];
353
- lines.push(`上游 HEAD: ${res.head}`);
354
- lines.push(`本地非独有: ${res.counts.local} 上游: ${res.counts.upstream}`);
355
- lines.push('');
356
- const totalDiff = res.result.added.length + res.result.updated.length + res.result.removed.length + res.result.renamed.length;
357
- if (totalDiff === 0) {
358
- lines.push('✅ 已是最新,无差异');
359
- } else {
360
- if (res.result.added.length) lines.push(`新增 (${res.result.added.length}): ${res.result.added.join(', ')}`);
361
- if (res.result.updated.length) lines.push(`更新 (${res.result.updated.length}): ${res.result.updated.join(', ')}`);
362
- if (res.result.renamed.length) lines.push(`重命名 (${res.result.renamed.length}): ${res.result.renamed.map((r) => `${r.from}→${r.to}`).join(', ')}`);
363
- if (res.result.removed.length) lines.push(`删除 (${res.result.removed.length}): ${res.result.removed.join(', ')}`);
364
- if (res.result.same.length) lines.push(`一致 (${res.result.same.length}): ${res.result.same.join(', ')}`);
365
- }
366
- process.stdout.write(lines.join('\n') + '\n');
367
- if (res.actions.length) {
368
- process.stdout.write(`\n已执行 ${res.actions.length} 项:\n`);
369
- for (const a of res.actions) process.stdout.write(` - ${a}\n`);
370
- }
371
- if (dryRun) process.stdout.write('\n(dry-run,未写文件)\n');
372
- }
373
- if (!dryRun && res.actions.length) {
374
- process.stdout.write('\n提示:请运行 npm test 验证,并按需执行 npm run build:template 更新模板镜像\n');
375
- }
376
- }
377
-
378
409
  async function main() {
379
410
  const args = process.argv.slice(2);
380
411
  if (args.length === 0 || args[0] === '--help' || args[0] === '-h') {
@@ -410,7 +441,8 @@ async function main() {
410
441
  return;
411
442
  }
412
443
  if (command === 'update') {
413
- await updateCommand(rest);
444
+ process.stderr.write('update 已合并到 sync --apply\n');
445
+ process.exitCode = 1;
414
446
  return;
415
447
  }
416
448
  process.stderr.write(HELP);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@heihei0299/matt-skills",
3
- "version": "1.3.0",
3
+ "version": "1.3.1",
4
4
  "description": "Agent skills + 项目配置模板:一条命令初始化 opencode / pi-agent 项目(含 mattpocock/skills 上游技能)",
5
5
  "type": "module",
6
6
  "bin": {
@@ -32,6 +32,10 @@ _Avoid_: inheriting repo, child repo
32
32
  The one-time action of setting up a Target Repository: copying `template/` into its root (`cp -r template/. <target>/`), then fetching the 22 upstream skills from the Upstream Repository. Copying, not inheriting — no runtime relationship survives the copy.
33
33
  _Avoid_: inherit, bootstrap
34
34
 
35
+ **Sync** (同步):
36
+ `matt-skills sync` 同步 Target Repository 的模板与上游技能:默认仅对比不写盘(`check`),`--apply` 为安全增量(`AGENTS.md` 有定制如 `tdd-implement` 则跳过,上游技能直接强制覆盖 `rm+cp` 但不 `remove`,独有技能与 `template/.opencode/.pi` 增量 `add/update`)、`--force` 为硬盖(`AGENTS.md` 备份 `.bak` 后强制覆盖,技能与模板全量 `add/update/remove`)。`update` 已合并到 `sync` 并删除。
37
+ _Avoid_: update, force sync
38
+
35
39
  ## Skill Design
36
40
 
37
41
  **Turn Continuity** (回合连续性):