@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 +12 -10
- package/bin/cli.js +231 -87
- package/package.json +1 -1
- package/template/AGENTS.md +2 -0
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`
|
|
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] [--
|
|
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
|
|
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
|
|
106
|
-
npx @heihei0299/matt-skills sync
|
|
107
|
-
npx @heihei0299/matt-skills sync
|
|
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
|
-
|
|
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
|
|
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
|
|
72
|
-
--
|
|
73
|
-
--
|
|
74
|
-
--
|
|
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
|
-
|
|
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)
|
|
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
|
|
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
|
|
378
|
-
const
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
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
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
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
|
-
//
|
|
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
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
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
|
-
|
|
480
|
-
|
|
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
|
-
|
|
636
|
+
output(`迁移提示:${preservedRepoLocal.join(', ')} 已不再分发,现有副本已保留\n`);
|
|
494
637
|
}
|
|
495
638
|
const modeLabel = onlyProgramming ? '默认编程' : '全部可分发';
|
|
496
|
-
|
|
497
|
-
|
|
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
|
|
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 === '--
|
|
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,
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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') {
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
834
|
+
process.exitCode = 2;
|
|
691
835
|
});
|
package/package.json
CHANGED