@heihei0299/matt-skills 3.0.13 → 3.0.15

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,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`。
@@ -64,7 +62,7 @@ npx @heihei0299/matt-skills sync --all # 同步全部可分发 skills
64
62
  npx @heihei0299/matt-skills sync --dry-run --json
65
63
  ```
66
64
 
67
- `init` 默认保护已有 `AGENTS.md`;需要刷新完整模板时使用 `init --all`。默认 `sync` 保留已有 `AGENTS.md` 和项目规则,`sync --all` 才将其整体刷新为当前分发模板。`sync` 不删除目标项目的额外文件或自定义 skills。
65
+ `init` 对已有 `AGENTS.md` 始终跳过;已有项目使用 `sync`。默认 `sync` 保留已有 `AGENTS.md` 和项目规则,`--all` 只扩大技能范围。需要显式刷新 `AGENTS.md` 时使用 `sync --refresh-agents`;无受管区块时会先备份为 `AGENTS.md.bak`。`sync` 不删除目标项目的额外文件或自定义 skills。
68
66
 
69
67
  默认 programming 范围中的 4 个独有 skills 是 `tdd-implement`、`diagnose-fix`、`grill-to-spec`、`show-me`。
70
68
 
@@ -74,17 +72,19 @@ npx @heihei0299/matt-skills sync --dry-run --json
74
72
  npx @heihei0299/matt-skills list [--all] [--json]
75
73
  npx @heihei0299/matt-skills install [--all] [--tools <list>] [--global] [--dest <dir>]
76
74
  npx @heihei0299/matt-skills init [--all] [--dest <dir>]
77
- npx @heihei0299/matt-skills sync [--all] [--dry-run] [--json] [--dest <dir>]
75
+ npx @heihei0299/matt-skills sync [--all] [--dry-run] [--refresh-agents] [--dest <dir>]
78
76
  npx @heihei0299/matt-skills check [--all] [--json] [--upstream <url>] [--ref <ref>]
79
77
  ```
80
78
 
81
79
  常用选项:
82
80
 
83
- - `--all`:包含全部可分发 skills,默认范围只包含 programming skills。
81
+ - `--all`:包含全部可分发 skills,默认范围只包含 programming skills;不会隐式刷新 `AGENTS.md`。
82
+ - `--refresh-agents`:显式刷新目标 `AGENTS.md`;与 `--all`、`--dry-run` 可组合。
84
83
  - `--dest <dir>`:指定目标目录。
85
84
  - `--tools <list>`:选择 `codex`、`pi`、`opencode` 或 `claude`;项目级 skills 统一写入 `.agents/skills/`,全局安装仍使用各工具目录。
86
85
  - `--global`:写入用户级 skills 目录。
87
- - `--dry-run`:只检查差异,不写入;`--json` 输出机器可读结果。
86
+ - `--dry-run`:预演目标项目差异,不写入;`sync --dry-run --json` 输出机器可读结果。
87
+ - `check --json`:输出上游比较的机器可读结果。普通 `sync --json` 不支持。
88
88
 
89
89
  ## Codex CLI 支持
90
90
 
@@ -102,14 +102,16 @@ CODEX_E2E=1 npm run codex:smoke
102
102
  非独有 skills 来自 [mattpocock/skills](https://github.com/mattpocock/skills)。
103
103
 
104
104
  ```sh
105
- npx @heihei0299/matt-skills sync --dry-run --json # 检查上游差异
106
- npx @heihei0299/matt-skills sync # 同步默认范围
107
- npx @heihei0299/matt-skills sync --all # 同步全部可分发范围
105
+ npx @heihei0299/matt-skills check --json # 检查上游差异
106
+ npx @heihei0299/matt-skills sync --dry-run --json # 预演目标项目变化
107
+ npx @heihei0299/matt-skills sync # 同步默认范围
108
+ npx @heihei0299/matt-skills sync --all # 同步全部可分发范围
109
+ npx @heihei0299/matt-skills sync --refresh-agents # 同步并刷新 AGENTS.md
108
110
  ```
109
111
 
110
112
  ## 发布
111
113
 
112
- 最近发布:`v3.0.12`。推送 `v*` 标签会触发 GitHub Actions:全量测试、模板检查和 npm 发布。
114
+ 版本由推送的 `v*` 标签决定;GitHub Actions 会在发布时从 tag 设置 npm 包版本,并执行全量测试与模板检查。
113
115
 
114
116
  ```sh
115
117
  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, mkdtemp } 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';
@@ -39,7 +39,7 @@ const HELP_GLOBAL = `matt-skills — install and manage this skill collection
39
39
 
40
40
  Usage:
41
41
  matt-skills init [options] Initialize a project: template + skills (${PROJECT_SKILL_DIRS})
42
- matt-skills sync [--all|--dry-run] [--dest <path>] Sync existing project to latest template + skills
42
+ matt-skills sync [--all|--dry-run|--refresh-agents] [--dest <path>] Sync existing project to latest template + skills
43
43
  matt-skills list [--all] [--json] List available skills and their descriptions
44
44
  matt-skills install [options] Install skills (interactive by default)
45
45
  matt-skills check [--all] [--json] [--upstream <url>] [--ref <ref>]
@@ -57,7 +57,7 @@ Init options:
57
57
  --dest <path> Target directory (default: current directory)
58
58
  --all Include all distributable skills; default only default programming skills
59
59
  --help, -h Show this help
60
- 提示:已有 AGENTS.md 时普通 init 跳过;显式 init --all 刷新模板并覆盖全量可分发 skills。
60
+ 提示:已有 AGENTS.md 时 init 始终跳过;需要更新已有项目请使用 sync。
61
61
 
62
62
  提示:matt-skills --help 查看全量
63
63
  `;
@@ -65,16 +65,19 @@ Init options:
65
65
  const HELP_SYNC = `matt-skills sync — Sync existing project to latest template + skills
66
66
 
67
67
  Usage:
68
- matt-skills sync [--all|--dry-run] [--dest <path>]
68
+ matt-skills sync [--all|--dry-run|--refresh-agents] [--dest <path>]
69
69
 
70
70
  Sync options:
71
- --all 仅更新同名技能内容(存在则覆盖,不存在则新增)并更新 AGENTS.md
72
- --dry-run 预演:只比对不写盘
73
- --dest <path> Target directory (default: current directory)
74
- --help, -h Show this help
71
+ --all 同步全部可分发技能;不改变 AGENTS.md 刷新策略
72
+ --refresh-agents 显式刷新 AGENTS.md;无受管区块时先备份为 AGENTS.md.bak
73
+ --dry-run 预演目标项目变化,只比对不写盘
74
+ --json 仅与 --dry-run 一起使用,输出机器可读结果
75
+ --dest <path> Target directory (default: current directory)
76
+ --help, -h Show this help
75
77
  项目 skills:${PROJECT_SKILL_DIRS}
76
78
 
77
- 说明:默认不带 --all 增量同步默认 programming skill,并保留现有 AGENTS.md;--all 时同步全部可分发 skill 并强制刷新 AGENTS.md。
79
+ 说明:默认同步默认编程 skill 并保留现有 AGENTS.md;--all 只扩大技能范围。
80
+ --refresh-agents 与 --all、--dry-run 可组合;上游检查请使用 check。
78
81
 
79
82
  提示:matt-skills --help 查看全量
80
83
  `;
@@ -126,6 +129,8 @@ Install options:
126
129
 
127
130
  const HELP = HELP_GLOBAL;
128
131
 
132
+ const DRY_RUN_PATHS = ['AGENTS.md', 'AGENTS.md.bak', '.opencode', '.pi', '.agents/skills', '.claude/skills'];
133
+
129
134
  function parseFrontmatter(text) {
130
135
  const match = text.match(/^---\r?\n([\s\S]*?)\r?\n---/);
131
136
  if (!match) return {};
@@ -236,6 +241,57 @@ function projectSkillTargets(target) {
236
241
  return [...targets.values()];
237
242
  }
238
243
 
244
+ const LEGACY_PROJECT_SKILL_DIRS = ['.pi/skills', '.opencode/skills', '.claude/skills'];
245
+
246
+ async function isSafeRealPath(targetPath) {
247
+ const absolute = path.resolve(targetPath);
248
+ const root = path.parse(absolute).root;
249
+ let current = root;
250
+ for (const segment of path.relative(root, absolute).split(path.sep).filter(Boolean)) {
251
+ current = path.join(current, segment);
252
+ try {
253
+ if ((await lstat(current)).isSymbolicLink()) return false;
254
+ } catch {
255
+ return false;
256
+ }
257
+ }
258
+ return true;
259
+ }
260
+
261
+ function sameMetadata(left, right) {
262
+ return (left.mode & 0o7777) === (right.mode & 0o7777)
263
+ && left.uid === right.uid
264
+ && left.gid === right.gid;
265
+ }
266
+
267
+ async function sameTree(left, right) {
268
+ try {
269
+ const [leftInfo, rightInfo] = await Promise.all([lstat(left), lstat(right)]);
270
+ if (leftInfo.isSymbolicLink() || rightInfo.isSymbolicLink()) return false;
271
+ if (leftInfo.isDirectory() !== rightInfo.isDirectory()) return false;
272
+ if (leftInfo.isDirectory()) {
273
+ const [leftEntries, rightEntries] = await Promise.all([
274
+ readdir(left, { withFileTypes: true }),
275
+ readdir(right, { withFileTypes: true }),
276
+ ]);
277
+ if (!sameMetadata(leftInfo, rightInfo) || leftEntries.length !== rightEntries.length) return false;
278
+ const rightNames = new Set(rightEntries.map((entry) => entry.name));
279
+ for (const entry of leftEntries) {
280
+ if (!rightNames.has(entry.name) || !await sameTree(path.join(left, entry.name), path.join(right, entry.name))) {
281
+ return false;
282
+ }
283
+ }
284
+ return true;
285
+ }
286
+ if (leftInfo.isFile() && rightInfo.isFile()) {
287
+ return (await readFile(left)).equals(await readFile(right)) && sameMetadata(leftInfo, rightInfo);
288
+ }
289
+ return false;
290
+ } catch {
291
+ return false;
292
+ }
293
+ }
294
+
239
295
  const GLOBAL_DIRS = {
240
296
  codex: '.codex/skills',
241
297
  pi: '.pi/agent/skills',
@@ -326,7 +382,7 @@ async function initCommand({ dest, all }) {
326
382
  const target = dest ? path.resolve(process.cwd(), dest) : process.cwd();
327
383
  const marker = path.join(target, 'AGENTS.md');
328
384
  const onlyProgramming = !all;
329
- if (await pathExists(marker) && !all) {
385
+ if (await pathExists(marker)) {
330
386
  process.stdout.write('模板已存在(AGENTS.md),跳过\n');
331
387
  } else {
332
388
  await cp(TEMPLATE_DIR, target, {
@@ -371,82 +427,141 @@ async function initCommand({ dest, all }) {
371
427
  }
372
428
  process.stdout.write(`目标路径:${target}\n`);
373
429
  }
374
- async function syncCommand({ dest, all, dryRun, json, upstreamUrl, ref }) {
430
+ async function copyDryRunInputs(target, stage) {
431
+ for (const relative of DRY_RUN_PATHS) {
432
+ const source = path.join(target, relative);
433
+ if (!await pathExists(source)) continue;
434
+ const destination = path.join(stage, relative);
435
+ await mkdir(path.dirname(destination), { recursive: true });
436
+ await cp(source, destination, { recursive: true, force: true });
437
+ }
438
+ }
439
+
440
+ async function compareDryRunTrees(target, stage) {
441
+ const result = { added: [], updated: [], removed: [] };
442
+ for (const relative of DRY_RUN_PATHS) {
443
+ const before = path.join(target, relative);
444
+ const after = path.join(stage, relative);
445
+ const beforeExists = await pathExists(before);
446
+ const afterExists = await pathExists(after);
447
+ if (!beforeExists && afterExists) result.added.push(relative);
448
+ else if (beforeExists && !afterExists) result.removed.push(relative);
449
+ else if (beforeExists && afterExists && !await sameTree(before, after)) result.updated.push(relative);
450
+ }
451
+ return result;
452
+ }
453
+
454
+ function formatTargetComparison({ target, result, onlyProgramming, refreshAgents }) {
455
+ const lines = [
456
+ `目标: ${target}`,
457
+ `范围: ${onlyProgramming ? '默认编程' : '全部可分发'}${refreshAgents ? ';刷新 AGENTS.md' : ''}`,
458
+ '',
459
+ ];
460
+ const totalDiff = result.added.length + result.updated.length + result.removed.length;
461
+ if (totalDiff === 0) {
462
+ lines.push('✅ 无需更新');
463
+ return lines.join('\n');
464
+ }
465
+ if (result.added.length) lines.push(`新增 (${result.added.length}): ${result.added.join(', ')}`);
466
+ if (result.updated.length) lines.push(`更新 (${result.updated.length}): ${result.updated.join(', ')}`);
467
+ if (result.removed.length) lines.push(`删除 (${result.removed.length}): ${result.removed.join(', ')}`);
468
+ return lines.join('\n');
469
+ }
470
+
471
+ async function refreshAgentsFile(targetFile) {
472
+ const templateFile = path.join(TEMPLATE_DIR, 'AGENTS.md');
473
+ const current = await readFile(targetFile, 'utf8');
474
+ const template = await readFile(templateFile, 'utf8');
475
+ const merged = mergeManagedAgents(current, template);
476
+ if (merged !== null) {
477
+ if (merged !== current) await writeFile(targetFile, merged);
478
+ return 'managed';
479
+ }
480
+ await cp(targetFile, `${targetFile}.bak`, { force: true });
481
+ await cp(templateFile, targetFile, { force: true });
482
+ return 'full';
483
+ }
484
+
485
+ async function syncCommand({ dest, all, dryRun, json, refreshAgents, quiet = false }) {
375
486
  const onlyProgramming = !all;
487
+ const output = (text) => {
488
+ if (!quiet) process.stdout.write(text);
489
+ };
490
+ if (!dryRun && json) throw new Error('--json 仅支持 sync --dry-run');
376
491
  if (dryRun) {
377
- const { compare, formatComparison } = await import('../scripts/sync-upstream.js');
378
- const cmp = await compare({ upstreamUrl, ref, onlyProgramming });
379
- if (json) {
380
- process.stdout.write(JSON.stringify({ head: cmp.head, counts: cmp.counts, result: cmp.result, onlyProgramming }, null, 2) + '\n');
381
- } else {
382
- process.stdout.write(formatComparison(cmp) + '\n');
492
+ const target = dest ? path.resolve(process.cwd(), dest) : process.cwd();
493
+ const stage = await mkdtemp(path.join(os.tmpdir(), 'matt-skills-sync-dry-run-'));
494
+ try {
495
+ await copyDryRunInputs(target, stage);
496
+ await syncCommand({ dest: stage, all, dryRun: false, json: false, refreshAgents, quiet: true });
497
+ const result = await compareDryRunTrees(target, stage);
498
+ const payload = { target, result, onlyProgramming, refreshAgents };
499
+ if (json) output(`${JSON.stringify(payload, null, 2)}\n`);
500
+ else output(`${formatTargetComparison(payload)}\n`);
501
+ const totalDiff = result.added.length + result.updated.length + result.removed.length;
502
+ if (totalDiff > 0) process.exitCode = 1;
503
+ } finally {
504
+ await rm(stage, { recursive: true, force: true });
383
505
  }
384
- const { rm } = await import('node:fs/promises');
385
- await rm(cmp.dest, { recursive: true, force: true });
386
- const totalDiff = cmp.result.added.length + cmp.result.updated.length + cmp.result.removed.length + cmp.result.renamed.length;
387
- if (totalDiff > 0) process.exitCode = 1;
388
506
  return;
389
507
  }
390
508
 
391
509
  const target = dest ? path.resolve(process.cwd(), dest) : process.cwd();
392
510
  const marker = path.join(target, 'AGENTS.md');
393
- // 模板同步:默认模式下过滤 skills,仅同步默认子集
394
511
  async function copyTemplateFiltered() {
395
- if (!onlyProgramming) {
396
- await cp(TEMPLATE_DIR, target, {
397
- recursive: true,
398
- force: true,
399
- filter: shouldCopyTemplatePath,
400
- });
401
- return;
402
- }
403
- // Skeleton only; shared Skills are copied from the canonical source below.
404
- await cp(path.join(TEMPLATE_DIR, 'AGENTS.md'), path.join(target, 'AGENTS.md'), { force: true });
405
- await cp(path.join(TEMPLATE_DIR, '.opencode'), path.join(target, '.opencode'), { recursive: true, force: true });
406
- await cp(path.join(TEMPLATE_DIR, '.pi'), path.join(target, '.pi'), { recursive: true, force: true });
407
- }
408
- if (!(await pathExists(marker))) {
409
- process.stdout.write('未检测到现有项目(AGENTS.md 不存在),将执行全新初始化\n');
410
- if (onlyProgramming) await copyTemplateFiltered();
411
- else await cp(TEMPLATE_DIR, target, {
512
+ await cp(TEMPLATE_DIR, target, {
412
513
  recursive: true,
413
514
  force: true,
414
515
  filter: shouldCopyTemplatePath,
415
516
  });
416
- process.stdout.write(`模板:已复制(AGENTS.md、skills:${PROJECT_SKILL_DIRS})\n`);
517
+ }
518
+ if (!(await pathExists(marker))) {
519
+ output('未检测到现有项目(AGENTS.md 不存在),将执行全新初始化\n');
520
+ await copyTemplateFiltered();
521
+ output(`模板:已复制(AGENTS.md、skills:${PROJECT_SKILL_DIRS})\n`);
417
522
  } else {
418
- process.stdout.write('同步:检测到现有项目,将增量更新\n');
419
- if (all) {
420
- await cp(TEMPLATE_DIR, target, {
421
- recursive: true,
422
- force: true,
423
- filter: shouldCopyTemplatePath,
424
- });
425
- process.stdout.write(`模板:已同步(AGENTS.md 整体刷新、skills:${PROJECT_SKILL_DIRS})\n`);
523
+ output('同步:检测到现有项目,将增量更新\n');
524
+ let agentsManaged = false;
525
+ let agentsRefreshed = false;
526
+ let agentsRefreshMode = null;
527
+ if (refreshAgents) {
528
+ agentsRefreshMode = await refreshAgentsFile(marker);
529
+ agentsRefreshed = true;
426
530
  } else {
427
- let agentsManaged = false;
428
531
  try {
429
532
  agentsManaged = await syncManagedAgents(marker);
430
533
  } catch {}
431
- await cp(path.join(TEMPLATE_DIR, '.opencode'), path.join(target, '.opencode'), { recursive: true, force: true });
432
- await cp(path.join(TEMPLATE_DIR, '.pi'), path.join(target, '.pi'), { recursive: true, force: true });
433
- if (agentsManaged) {
434
- process.stdout.write(`模板:已同步(AGENTS.md 受管区块已更新、skills:${PROJECT_SKILL_DIRS},项目自定义内容已保留)\n`);
435
- } else {
436
- process.stdout.write(`模板:已同步(AGENTS.md 未受管、skills:${PROJECT_SKILL_DIRS},已原样保留)\n`);
437
- }
534
+ }
535
+ await cp(path.join(TEMPLATE_DIR, '.opencode'), path.join(target, '.opencode'), { recursive: true, force: true });
536
+ await cp(path.join(TEMPLATE_DIR, '.pi'), path.join(target, '.pi'), { recursive: true, force: true });
537
+ if (agentsRefreshed && agentsRefreshMode === 'managed') {
538
+ output(`模板:已同步(AGENTS.md 受管区块已刷新、skills:${PROJECT_SKILL_DIRS},项目自定义内容已保留)\n`);
539
+ } else if (agentsRefreshed) {
540
+ output(`模板:已同步(AGENTS.md 已刷新,旧文件备份为 AGENTS.md.bak、skills:${PROJECT_SKILL_DIRS})\n`);
541
+ } else if (agentsManaged) {
542
+ output(`模板:已同步(AGENTS.md 受管区块已更新、skills:${PROJECT_SKILL_DIRS},项目自定义内容已保留)\n`);
543
+ } else {
544
+ output(`模板:已同步(AGENTS.md 未受管、skills:${PROJECT_SKILL_DIRS},已原样保留)\n`);
438
545
  }
439
546
  }
440
- // 技能同步:--all 仅更新同名可分发技能内容,存在则覆盖,不存在则新增,并更新 AGENTS.md(由上一步已处理);默认范围为默认 programming,不删多余
547
+ // --all 只扩大技能范围;同名覆盖、不存在新增,不删除目标中的额外技能。
441
548
  const allSkills = await listSkillNames({ onlyProgramming });
442
549
  const skillTargets = projectSkillTargets(target);
443
550
  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),
551
+ const legacyLocations = LEGACY_PROJECT_SKILL_DIRS.map((relative) => ({
552
+ dir: path.resolve(target, relative),
553
+ label: relative,
554
+ isWorkspaceSource: false,
449
555
  }));
556
+ const preservedRepoLocal = [];
557
+ const preserveLocations = [
558
+ ...skillTargets.map(({ tool, dir }) => ({
559
+ dir,
560
+ label: PROJECT_DIRS[tool],
561
+ isWorkspaceSource: path.resolve(dir) === path.resolve(SKILLS_DIR),
562
+ })),
563
+ ...legacyLocations,
564
+ ];
450
565
  for (const { dir } of skillTargets) await mkdir(dir, { recursive: true });
451
566
  for (const name of REPO_LOCAL_SKILLS) {
452
567
  for (const location of preserveLocations) {
@@ -476,25 +591,53 @@ async function syncCommand({ dest, all, dryRun, json, upstreamUrl, ref }) {
476
591
  }
477
592
  }
478
593
  }
479
- // Project skill directories may contain project-local Skills. Preserve them because
480
- // their origin cannot be distinguished safely from a historical shared mirror.
594
+ const distributableSkillNames = (await listSkillNames({ onlyProgramming: false })).filter((name) => !REPO_LOCAL_SKILLS.has(name));
595
+ const cleanedLegacy = [];
596
+ for (const location of legacyLocations) {
597
+ let entries;
598
+ try {
599
+ if (!await isSafeRealPath(location.dir)) continue;
600
+ const locationInfo = await lstat(location.dir);
601
+ if (!locationInfo.isDirectory()) continue;
602
+ entries = await readdir(location.dir, { withFileTypes: true });
603
+ } catch {
604
+ continue;
605
+ }
606
+ for (const entry of entries) {
607
+ if (!entry.isDirectory() || entry.name === '.git' || entry.name.endsWith('.bak')) continue;
608
+ if (!distributableSkillNames.includes(entry.name)) continue;
609
+ const src = path.join(SKILLS_DIR, entry.name);
610
+ const dst = path.join(location.dir, entry.name);
611
+ try {
612
+ if (await sameTree(src, dst)) {
613
+ await rm(dst, { recursive: true, force: true });
614
+ cleanedLegacy.push(`${location.label}/${entry.name}`);
615
+ }
616
+ } catch {}
617
+ }
618
+ }
619
+ // Legacy harness dirs may contain project-local Skills. Only exact canonical
620
+ // trees of clearly distributable skills are removed; repo-local, modified or
621
+ // unknown trees remain untouched.
481
622
  // 清理过时的 .pi/settings.json 指向
482
623
  try {
483
624
  const piSettings = path.join(target, '.pi/settings.json');
484
625
  if (await pathExists(piSettings)) {
485
626
  const txt = await readFile(piSettings, 'utf8');
486
627
  if (txt.includes('.opencode/skills') || txt.includes('../.opencode')) {
487
- const { writeFile } = await import('node:fs/promises');
488
628
  await writeFile(piSettings, '{}\n');
489
629
  }
490
630
  }
491
631
  } catch {}
632
+ if (cleanedLegacy.length) {
633
+ output(`迁移清理:${cleanedLegacy.join(', ')} 旧共享副本已移除\n`);
634
+ }
492
635
  if (preservedRepoLocal.length) {
493
- process.stdout.write(`迁移提示:${preservedRepoLocal.join(', ')} 已不再分发,现有副本已保留\n`);
636
+ output(`迁移提示:${preservedRepoLocal.join(', ')} 已不再分发,现有副本已保留\n`);
494
637
  }
495
638
  const modeLabel = onlyProgramming ? '默认编程' : '全部可分发';
496
- process.stdout.write(`技能:新增 ${installed}、更新 ${updated}(${modeLabel} ${allSkills.length})\n`);
497
- process.stdout.write(`目标路径:${target}\n`);
639
+ output(`技能:新增 ${installed}、更新 ${updated}(${modeLabel} ${allSkills.length})\n`);
640
+ output(`目标路径:${target}\n`);
498
641
  }
499
642
 
500
643
  function parseInitArgs(args) {
@@ -519,8 +662,7 @@ function parseSyncArgs(args) {
519
662
  let all = false;
520
663
  let dryRun = false;
521
664
  let json = false;
522
- let upstreamUrl;
523
- let ref;
665
+ let refreshAgents = false;
524
666
  for (let i = 0; i < args.length; i++) {
525
667
  const arg = args[i];
526
668
  if (arg === '--dest') {
@@ -530,20 +672,13 @@ function parseSyncArgs(args) {
530
672
  else if (arg === '--all') all = true;
531
673
  else if (arg === '--dry-run') dryRun = true;
532
674
  else if (arg === '--json') json = true;
533
- else if (arg === '--upstream') {
534
- if (i + 1 >= args.length || args[i + 1].startsWith('-')) throw new Error(`unknown option '--upstream' requires a value`);
535
- upstreamUrl = args[++i];
536
- } else if (arg.startsWith('--upstream=')) upstreamUrl = arg.slice('--upstream='.length);
537
- else if (arg === '--ref') {
538
- if (i + 1 >= args.length || args[i + 1].startsWith('-')) throw new Error(`unknown option '--ref' requires a value`);
539
- ref = args[++i];
540
- } else if (arg.startsWith('--ref=')) ref = arg.slice('--ref='.length);
675
+ else if (arg === '--refresh-agents') refreshAgents = true;
541
676
  else if (arg === '--help' || arg === '-h') {} // handled at main
542
677
  else if (arg === '--apply') {} // deprecated alias, same as default safe incremental
543
678
  else if (arg.startsWith('-')) throw new Error(`unknown option '${arg}' for command 'sync'`);
544
679
  else throw new Error(`unknown argument '${arg}' for command 'sync'`);
545
680
  }
546
- return { dest, all, dryRun, json, upstreamUrl, ref };
681
+ return { dest, all, dryRun, json, refreshAgents };
547
682
  }
548
683
 
549
684
  function parseInstallArgs(args) {
@@ -633,15 +768,15 @@ async function main() {
633
768
  if (!knownCommands.has(command)) {
634
769
  process.stderr.write(`error: unknown command '${command}'\n`);
635
770
  process.stderr.write(`Run 'matt-skills --help' for usage.\n`);
636
- process.exitCode = 1;
771
+ process.exitCode = 2;
637
772
  return;
638
773
  }
639
774
  if (command === 'list') {
640
775
  // list strict: only --all/--json/--help allowed, rest handled via parse but we keep simple
641
776
  for (const a of rest) {
642
777
  if (a === '--all' || a === '--json' || a === '--help' || a === '-h') continue;
643
- if (a.startsWith('-')) { process.stderr.write(`error: unknown option '${a}' for command 'list'\n`); process.stderr.write(`Run 'matt-skills list --help' for usage.\n`); process.exitCode = 1; return; }
644
- process.stderr.write(`error: unknown argument '${a}' for command 'list'\n`); process.stderr.write(`Run 'matt-skills list --help' for usage.\n`); process.exitCode = 1; return;
778
+ if (a.startsWith('-')) { process.stderr.write(`error: unknown option '${a}' for command 'list'\n`); process.stderr.write(`Run 'matt-skills list --help' for usage.\n`); process.exitCode = 2; return; }
779
+ process.stderr.write(`error: unknown argument '${a}' for command 'list'\n`); process.stderr.write(`Run 'matt-skills list --help' for usage.\n`); process.exitCode = 2; return;
645
780
  }
646
781
  const onlyProgramming = !rest.includes('--all');
647
782
  const skills = await listSkills({ onlyProgramming });
@@ -671,21 +806,30 @@ async function main() {
671
806
  for (let i = 0; i < rest.length; i++) {
672
807
  const a = rest[i];
673
808
  if (a === '--all' || a === '--json' || a === '--help' || a === '-h') continue;
674
- if (a === '--upstream' || a === '--ref') { i++; continue; }
809
+ if (a === '--upstream' || a === '--ref') {
810
+ if (i + 1 >= rest.length || rest[i + 1].startsWith('-')) {
811
+ process.stderr.write(`error: option '${a}' for command 'check' requires a value\n`);
812
+ process.stderr.write(`Run 'matt-skills check --help' for usage.\n`);
813
+ process.exitCode = 2;
814
+ return;
815
+ }
816
+ i++;
817
+ continue;
818
+ }
675
819
  if (a.startsWith('--upstream=') || a.startsWith('--ref=')) continue;
676
- if (a.startsWith('-')) { process.stderr.write(`error: unknown option '${a}' for command 'check'\n`); process.stderr.write(`Run 'matt-skills check --help' for usage.\n`); process.exitCode = 1; return; }
677
- process.stderr.write(`error: unknown argument '${a}' for command 'check'\n`); process.stderr.write(`Run 'matt-skills check --help' for usage.\n`); process.exitCode = 1; return;
820
+ if (a.startsWith('-')) { process.stderr.write(`error: unknown option '${a}' for command 'check'\n`); process.stderr.write(`Run 'matt-skills check --help' for usage.\n`); process.exitCode = 2; return; }
821
+ process.stderr.write(`error: unknown argument '${a}' for command 'check'\n`); process.stderr.write(`Run 'matt-skills check --help' for usage.\n`); process.exitCode = 2; return;
678
822
  }
679
823
  await checkCommand(rest);
680
824
  return;
681
825
  }
682
826
  if (command === 'update') {
683
827
  process.stderr.write('update 已合并到 sync(默认即增量同步)\n');
684
- process.exitCode = 1;
828
+ process.exitCode = 2;
685
829
  return;
686
830
  }
687
831
  }
688
832
  main().catch((error) => {
689
833
  process.stderr.write(`error: ${error.message}\n`);
690
- process.exitCode = 1;
834
+ process.exitCode = 2;
691
835
  });
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.15",
4
4
  "description": "Agent skills + 项目配置模板:一条命令初始化 opencode / pi-agent 项目(含 mattpocock/skills 上游技能)",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,3 +1,4 @@
1
+ <!-- matt-skills:managed:start -->
1
2
  # AGENTS.md
2
3
 
3
4
  ## Workflow
@@ -75,3 +76,4 @@ codegraph node "<符号>"
75
76
  ## Completion
76
77
 
77
78
  完成时说明改动或审查结论、已执行验证、未执行验证及原因、剩余风险;如有 commit,报告 commit hash。
79
+ <!-- matt-skills:managed:end -->