@heihei0299/matt-skills 3.0.29 → 3.1.0
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 +4 -3
- package/bin/cli.js +15 -665
- package/bin/commands/check.js +48 -0
- package/bin/commands/init.js +63 -0
- package/bin/commands/install.js +114 -0
- package/bin/commands/list.js +26 -0
- package/bin/commands/sync.js +96 -0
- package/bin/project/agents.js +29 -0
- package/bin/project/filesystem.js +60 -0
- package/bin/project/skills.js +144 -0
- package/bin/project/template.js +101 -0
- package/bin/{skill-boundaries.js → skills/boundaries.js} +1 -1
- package/bin/skills/discovery.js +63 -0
- package/bin/{skill-selection.js → skills/selection.js} +1 -1
- package/config/default.json +0 -4
- package/config/proprietary.json +0 -2
- package/package.json +1 -1
- package/scripts/sync-upstream.js +2 -2
- package/template/AGENTS.md +11 -8
- package/template/.opencode/agents/issue-audit.md +0 -52
- package/template/.opencode/commands/grill-to-spec.md +0 -13
- package/template/.opencode/commands/handoff.md +0 -12
- package/template/.opencode/commands/improve-codebase-architecture.md +0 -13
- package/template/.opencode/commands/issue-audit.md +0 -115
- package/template/.opencode/commands/teach.md +0 -12
- package/template/.opencode/commands/to-spec.md +0 -13
- package/template/.opencode/commands/to-tickets.md +0 -12
- package/template/.opencode/commands/triage.md +0 -12
- package/template/.opencode/commands/wayfinder.md +0 -13
- package/template/.opencode/commands/writing-for-agents.md +0 -12
- package/template/.pi/agents/issue-audit.md +0 -52
- package/template/.pi/prompts/issue-audit.md +0 -114
- /package/bin/{skill-config.js → skills/config.js} +0 -0
package/bin/cli.js
CHANGED
|
@@ -1,37 +1,23 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
import os from 'node:os';
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
4
3
|
import path from 'node:path';
|
|
5
4
|
import { fileURLToPath } from 'node:url';
|
|
6
|
-
import
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
} from './skill-boundaries.js';
|
|
13
|
-
import { resolveSkillNames } from './skill-selection.js';
|
|
14
|
-
import { loadSkillSet } from './skill-config.js';
|
|
5
|
+
import { runCheck } from './commands/check.js';
|
|
6
|
+
import { runInit } from './commands/init.js';
|
|
7
|
+
import { runInstall } from './commands/install.js';
|
|
8
|
+
import { runList } from './commands/list.js';
|
|
9
|
+
import { runSync } from './commands/sync.js';
|
|
10
|
+
import { PROJECT_SKILLS_DIR } from './project/skills.js';
|
|
15
11
|
|
|
16
|
-
const SKILLS_DIR = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', '.agents', 'skills');
|
|
17
|
-
const TEMPLATE_DIR = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'template');
|
|
18
|
-
const DEFAULT_PATH = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'config', 'default.json');
|
|
19
|
-
let DEFAULT_SKILLS = null;
|
|
20
|
-
async function loadDefaultSkills() {
|
|
21
|
-
if (!DEFAULT_SKILLS) DEFAULT_SKILLS = await loadSkillSet(DEFAULT_PATH, 'default');
|
|
22
|
-
return DEFAULT_SKILLS;
|
|
23
|
-
}
|
|
24
12
|
process.stdout.on('error', (err) => {
|
|
25
13
|
if (err.code === 'EPIPE') process.exit(0);
|
|
26
14
|
throw err;
|
|
27
15
|
});
|
|
28
16
|
|
|
29
|
-
const PROJECT_SKILL_DIRS = '.agents/skills';
|
|
30
|
-
|
|
31
17
|
const HELP_GLOBAL = `matt-skills — install and manage this skill collection
|
|
32
18
|
|
|
33
19
|
Usage:
|
|
34
|
-
matt-skills init [options] Initialize a project: template + skills (${
|
|
20
|
+
matt-skills init [options] Initialize a project: template + skills (${PROJECT_SKILLS_DIR})
|
|
35
21
|
matt-skills sync [--all|--dry-run|--refresh-agents] [--dest <path>] Sync existing project to latest template + skills
|
|
36
22
|
matt-skills list [--all] [--json] List available skills and their descriptions
|
|
37
23
|
matt-skills install [options] Install skills (interactive by default)
|
|
@@ -41,7 +27,7 @@ Usage:
|
|
|
41
27
|
matt-skills --version | -v Show version
|
|
42
28
|
`;
|
|
43
29
|
|
|
44
|
-
const HELP_INIT = `matt-skills init [options] — Initialize a project: template + skills (${
|
|
30
|
+
const HELP_INIT = `matt-skills init [options] — Initialize a project: template + skills (${PROJECT_SKILLS_DIR})
|
|
45
31
|
|
|
46
32
|
Usage:
|
|
47
33
|
matt-skills init [options]
|
|
@@ -67,7 +53,7 @@ Sync options:
|
|
|
67
53
|
--json 仅与 --dry-run 一起使用,输出机器可读结果
|
|
68
54
|
--dest <path> Target directory (default: current directory)
|
|
69
55
|
--help, -h Show this help
|
|
70
|
-
项目 skills:${
|
|
56
|
+
项目 skills:${PROJECT_SKILLS_DIR}
|
|
71
57
|
|
|
72
58
|
说明:默认同步默认 workflow skill 并保留现有 AGENTS.md;--all 只扩大技能范围。
|
|
73
59
|
--refresh-agents 与 --all、--dry-run 可组合;上游检查请使用 check。
|
|
@@ -120,610 +106,6 @@ Install options:
|
|
|
120
106
|
提示:matt-skills --help 查看全量
|
|
121
107
|
`;
|
|
122
108
|
|
|
123
|
-
const HELP = HELP_GLOBAL;
|
|
124
|
-
|
|
125
|
-
const DRY_RUN_PATHS = ['AGENTS.md', 'AGENTS.md.bak', '.opencode', '.pi', '.agents/skills', '.claude/skills'];
|
|
126
|
-
|
|
127
|
-
function parseFrontmatter(text) {
|
|
128
|
-
const match = text.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
129
|
-
if (!match) return {};
|
|
130
|
-
const fields = {};
|
|
131
|
-
for (const line of match[1].split(/\r?\n/)) {
|
|
132
|
-
const m = line.match(/^([A-Za-z0-9_-]+):\s*(.*)$/);
|
|
133
|
-
if (!m) continue;
|
|
134
|
-
let value = m[2].trim();
|
|
135
|
-
if (value.startsWith('"') && value.endsWith('"')) {
|
|
136
|
-
value = value.slice(1, -1).replace(/\\"/g, '"').replace(/\\\\/g, '\\');
|
|
137
|
-
}
|
|
138
|
-
fields[m[1]] = value;
|
|
139
|
-
}
|
|
140
|
-
return fields;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
async function listAvailableSkillNames() {
|
|
144
|
-
const entries = await readdir(SKILLS_DIR, { withFileTypes: true });
|
|
145
|
-
return entries
|
|
146
|
-
.filter((entry) => entry.isDirectory())
|
|
147
|
-
.filter((entry) => !entry.name.endsWith('.bak'))
|
|
148
|
-
.filter((entry) => entry.name !== 'skill-creator' && entry.name !== '.git')
|
|
149
|
-
.map((entry) => entry.name);
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
async function listSkillNames({ onlyProgramming = false } = {}) {
|
|
153
|
-
return resolveSkillNames({
|
|
154
|
-
availableNames: await listAvailableSkillNames(),
|
|
155
|
-
mode: onlyProgramming ? 'default' : 'all',
|
|
156
|
-
defaults: onlyProgramming ? await loadDefaultSkills() : [],
|
|
157
|
-
});
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
async function listSkills({ onlyProgramming = false } = {}) {
|
|
161
|
-
const skills = [];
|
|
162
|
-
for (const name of await listSkillNames({ onlyProgramming })) {
|
|
163
|
-
let content;
|
|
164
|
-
try {
|
|
165
|
-
content = await readFile(path.join(SKILLS_DIR, name, 'SKILL.md'), 'utf8');
|
|
166
|
-
} catch {
|
|
167
|
-
continue;
|
|
168
|
-
}
|
|
169
|
-
const { description } = parseFrontmatter(content);
|
|
170
|
-
if (description) skills.push({ name, description });
|
|
171
|
-
}
|
|
172
|
-
return skills.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
async function pathExists(p) {
|
|
176
|
-
try {
|
|
177
|
-
await stat(p);
|
|
178
|
-
return true;
|
|
179
|
-
} catch {
|
|
180
|
-
return false;
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
function shouldCopyTemplatePath(src) {
|
|
185
|
-
const relative = path.relative(TEMPLATE_DIR, src);
|
|
186
|
-
const parts = relative.split(path.sep);
|
|
187
|
-
return !(parts[0] === '.agents' && parts[1] === 'skills');
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
const AGENTS_MANAGED_START = '<!-- matt-skills:managed:start -->';
|
|
191
|
-
const AGENTS_MANAGED_END = '<!-- matt-skills:managed:end -->';
|
|
192
|
-
|
|
193
|
-
function findManagedBlock(content) {
|
|
194
|
-
const start = content.indexOf(AGENTS_MANAGED_START);
|
|
195
|
-
if (start < 0 || content.indexOf(AGENTS_MANAGED_START, start + AGENTS_MANAGED_START.length) >= 0) return null;
|
|
196
|
-
const endStart = content.indexOf(AGENTS_MANAGED_END, start + AGENTS_MANAGED_START.length);
|
|
197
|
-
if (endStart < 0 || content.indexOf(AGENTS_MANAGED_END, endStart + AGENTS_MANAGED_END.length) >= 0) return null;
|
|
198
|
-
return { start, end: endStart + AGENTS_MANAGED_END.length };
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
function mergeManagedAgents(current, template) {
|
|
202
|
-
const currentBlock = findManagedBlock(current);
|
|
203
|
-
const templateBlock = findManagedBlock(template);
|
|
204
|
-
if (!currentBlock || !templateBlock) return null;
|
|
205
|
-
const managed = template.slice(templateBlock.start, templateBlock.end);
|
|
206
|
-
return current.slice(0, currentBlock.start) + managed + current.slice(currentBlock.end);
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
async function syncManagedAgents(targetFile) {
|
|
210
|
-
const current = await readFile(targetFile, 'utf8');
|
|
211
|
-
const template = await readFile(path.join(TEMPLATE_DIR, 'AGENTS.md'), 'utf8');
|
|
212
|
-
const merged = mergeManagedAgents(current, template);
|
|
213
|
-
if (merged === null) return false;
|
|
214
|
-
if (merged !== current) await writeFile(targetFile, merged);
|
|
215
|
-
return true;
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
const TOOLS = ['codex', 'pi', 'opencode', 'claude'];
|
|
219
|
-
|
|
220
|
-
const PROJECT_DIRS = {
|
|
221
|
-
codex: '.agents/skills',
|
|
222
|
-
pi: '.agents/skills',
|
|
223
|
-
opencode: '.agents/skills',
|
|
224
|
-
claude: '.agents/skills',
|
|
225
|
-
};
|
|
226
|
-
|
|
227
|
-
function projectSkillTargets(target) {
|
|
228
|
-
const targets = new Map();
|
|
229
|
-
for (const tool of TOOLS) {
|
|
230
|
-
const dir = path.resolve(target, PROJECT_DIRS[tool]);
|
|
231
|
-
if (!targets.has(dir)) targets.set(dir, { tool, dir });
|
|
232
|
-
}
|
|
233
|
-
return [...targets.values()];
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
const LEGACY_PROJECT_SKILL_DIRS = ['.pi/skills', '.opencode/skills', '.claude/skills'];
|
|
237
|
-
|
|
238
|
-
async function isSafeRealPath(targetPath) {
|
|
239
|
-
const absolute = path.resolve(targetPath);
|
|
240
|
-
const root = path.parse(absolute).root;
|
|
241
|
-
let current = root;
|
|
242
|
-
for (const segment of path.relative(root, absolute).split(path.sep).filter(Boolean)) {
|
|
243
|
-
current = path.join(current, segment);
|
|
244
|
-
try {
|
|
245
|
-
if ((await lstat(current)).isSymbolicLink()) return false;
|
|
246
|
-
} catch {
|
|
247
|
-
return false;
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
return true;
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
function sameMetadata(left, right) {
|
|
254
|
-
return (left.mode & 0o7777) === (right.mode & 0o7777)
|
|
255
|
-
&& left.uid === right.uid
|
|
256
|
-
&& left.gid === right.gid;
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
async function sameTree(left, right) {
|
|
260
|
-
try {
|
|
261
|
-
const [leftInfo, rightInfo] = await Promise.all([lstat(left), lstat(right)]);
|
|
262
|
-
if (leftInfo.isSymbolicLink() || rightInfo.isSymbolicLink()) return false;
|
|
263
|
-
if (leftInfo.isDirectory() !== rightInfo.isDirectory()) return false;
|
|
264
|
-
if (leftInfo.isDirectory()) {
|
|
265
|
-
const [leftEntries, rightEntries] = await Promise.all([
|
|
266
|
-
readdir(left, { withFileTypes: true }),
|
|
267
|
-
readdir(right, { withFileTypes: true }),
|
|
268
|
-
]);
|
|
269
|
-
if (!sameMetadata(leftInfo, rightInfo) || leftEntries.length !== rightEntries.length) return false;
|
|
270
|
-
const rightNames = new Set(rightEntries.map((entry) => entry.name));
|
|
271
|
-
for (const entry of leftEntries) {
|
|
272
|
-
if (!rightNames.has(entry.name) || !await sameTree(path.join(left, entry.name), path.join(right, entry.name))) {
|
|
273
|
-
return false;
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
return true;
|
|
277
|
-
}
|
|
278
|
-
if (leftInfo.isFile() && rightInfo.isFile()) {
|
|
279
|
-
return (await readFile(left)).equals(await readFile(right)) && sameMetadata(leftInfo, rightInfo);
|
|
280
|
-
}
|
|
281
|
-
return false;
|
|
282
|
-
} catch {
|
|
283
|
-
return false;
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
const GLOBAL_DIRS = {
|
|
288
|
-
codex: '.codex/skills',
|
|
289
|
-
pi: '.pi/agent/skills',
|
|
290
|
-
opencode: '.config/opencode/skills',
|
|
291
|
-
claude: '.claude/skills',
|
|
292
|
-
};
|
|
293
|
-
|
|
294
|
-
function toolDir(tool, global) {
|
|
295
|
-
if (global) return path.join(os.homedir(), GLOBAL_DIRS[tool]);
|
|
296
|
-
return path.resolve(process.cwd(), PROJECT_DIRS[tool]);
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
async function promptTools() {
|
|
300
|
-
const res = await prompts({
|
|
301
|
-
type: 'multiselect',
|
|
302
|
-
name: 'tools',
|
|
303
|
-
message: '选择要安装到的工具',
|
|
304
|
-
choices: TOOLS.map((t) => ({ title: t, value: t })),
|
|
305
|
-
instructions: '空格勾选,回车确认',
|
|
306
|
-
});
|
|
307
|
-
return Array.isArray(res?.tools) ? res.tools : [];
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
async function promptSkills(skills) {
|
|
311
|
-
const res = await prompts({
|
|
312
|
-
type: 'autocompleteMultiselect',
|
|
313
|
-
name: 'skills',
|
|
314
|
-
message: '选择要安装的技能',
|
|
315
|
-
choices: skills.map((s) => ({ title: s.name, value: s.name })),
|
|
316
|
-
instructions: '输入过滤,空格勾选,回车确认',
|
|
317
|
-
});
|
|
318
|
-
return Array.isArray(res?.skills) ? res.skills : [];
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
async function installCommand({ dest, all, force, tools, global }) {
|
|
322
|
-
const onlyProgramming = !all;
|
|
323
|
-
const skillNames = await listSkillNames({ onlyProgramming });
|
|
324
|
-
const skills = await listSkills({ onlyProgramming });
|
|
325
|
-
let targets;
|
|
326
|
-
if (dest) {
|
|
327
|
-
targets = [{ tool: null, dir: path.resolve(process.cwd(), dest) }];
|
|
328
|
-
} else {
|
|
329
|
-
const selectedTools = tools
|
|
330
|
-
? tools.filter((t) => TOOLS.includes(t))
|
|
331
|
-
: await promptTools();
|
|
332
|
-
if (selectedTools.length === 0) {
|
|
333
|
-
process.stdout.write('未选择任何工具,未安装任何技能\n');
|
|
334
|
-
return;
|
|
335
|
-
}
|
|
336
|
-
targets = selectedTools.map((tool) => ({ tool, dir: toolDir(tool, global) }));
|
|
337
|
-
// 去重:保留对自定义 PROJECT_DIRS 映射的兼容。
|
|
338
|
-
const seen = new Map();
|
|
339
|
-
for (const t of targets) {
|
|
340
|
-
if (!seen.has(t.dir)) seen.set(t.dir, t);
|
|
341
|
-
}
|
|
342
|
-
targets = [...seen.values()];
|
|
343
|
-
}
|
|
344
|
-
const selected = all ? skillNames : await promptSkills(skills);
|
|
345
|
-
if (selected.length === 0) {
|
|
346
|
-
process.stdout.write('未选择任何技能,未安装任何技能\n');
|
|
347
|
-
return;
|
|
348
|
-
}
|
|
349
|
-
const knownDistributable = new Set(skillNames);
|
|
350
|
-
for (const name of selected) {
|
|
351
|
-
if (!isDistributableSkill(name, knownDistributable)) {
|
|
352
|
-
throw new Error(`${name} is repository-local or unavailable and cannot be distributed`);
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
for (const { tool, dir } of targets) {
|
|
356
|
-
let installed = 0;
|
|
357
|
-
let skipped = 0;
|
|
358
|
-
for (const name of selected) {
|
|
359
|
-
const dst = path.join(dir, name);
|
|
360
|
-
if (!force && (await pathExists(dst))) {
|
|
361
|
-
skipped++;
|
|
362
|
-
continue;
|
|
363
|
-
}
|
|
364
|
-
await cp(path.join(SKILLS_DIR, name), dst, { recursive: true, force: true });
|
|
365
|
-
installed++;
|
|
366
|
-
}
|
|
367
|
-
if (tool) process.stdout.write(`${tool}:已装 ${installed}、跳过 ${skipped}\n`);
|
|
368
|
-
else process.stdout.write(`已装 ${installed}、跳过 ${skipped}\n`);
|
|
369
|
-
process.stdout.write(`目标路径:${dir}\n`);
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
async function initCommand({ dest, all }) {
|
|
374
|
-
const target = dest ? path.resolve(process.cwd(), dest) : process.cwd();
|
|
375
|
-
const marker = path.join(target, 'AGENTS.md');
|
|
376
|
-
const onlyProgramming = !all;
|
|
377
|
-
if (await pathExists(marker)) {
|
|
378
|
-
process.stdout.write('模板已存在(AGENTS.md),跳过\n');
|
|
379
|
-
} else {
|
|
380
|
-
await cp(TEMPLATE_DIR, target, {
|
|
381
|
-
recursive: true,
|
|
382
|
-
force: true,
|
|
383
|
-
filter: shouldCopyTemplatePath,
|
|
384
|
-
});
|
|
385
|
-
const selectedSkills = await listSkillNames({ onlyProgramming });
|
|
386
|
-
for (const { dir } of projectSkillTargets(target)) {
|
|
387
|
-
await mkdir(dir, { recursive: true });
|
|
388
|
-
for (const name of selectedSkills) {
|
|
389
|
-
const source = path.join(SKILLS_DIR, name);
|
|
390
|
-
const destination = path.join(dir, name);
|
|
391
|
-
if (path.resolve(source) === path.resolve(destination)) continue;
|
|
392
|
-
await cp(source, destination, { recursive: true, force: true });
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
process.stdout.write(`模板:已复制(AGENTS.md、skills:${PROJECT_SKILL_DIRS})\n`);
|
|
396
|
-
}
|
|
397
|
-
// 统计(区分编程 vs 全量)
|
|
398
|
-
const skillsDir = path.join(target, '.agents', 'skills');
|
|
399
|
-
let installed = 0;
|
|
400
|
-
try {
|
|
401
|
-
const entries = await readdir(skillsDir, { withFileTypes: true });
|
|
402
|
-
installed = entries.filter((e) => e.isDirectory() && !e.name.endsWith('.bak') && e.name !== '.git' && e.name !== 'skill-creator' && !isRepoLocalSkill(e.name)).length;
|
|
403
|
-
} catch {}
|
|
404
|
-
const allSkillsFull = await listSkills({ onlyProgramming: false });
|
|
405
|
-
const defaultSkills = await listSkills({ onlyProgramming: true });
|
|
406
|
-
const defaultCount = defaultSkills.length;
|
|
407
|
-
const upstreamFull = allSkillsFull.filter((s) => !PROPRIETARY_SKILLS.has(s.name)).length;
|
|
408
|
-
const upstreamDefault = defaultSkills.filter((s) => !PROPRIETARY_SKILLS.has(s.name)).length;
|
|
409
|
-
const displayTotal = onlyProgramming ? defaultCount : allSkillsFull.length;
|
|
410
|
-
const displayUpstream = onlyProgramming ? upstreamDefault : upstreamFull;
|
|
411
|
-
if (path.resolve(skillsDir) === path.resolve(SKILLS_DIR)) {
|
|
412
|
-
process.stdout.write(`技能:已装 ${installed}、跳过 0(可分发 ${allSkillsFull.length},含上游 ${upstreamFull};默认 workflow ${defaultCount},含上游 ${upstreamDefault})\n`);
|
|
413
|
-
} else {
|
|
414
|
-
if (onlyProgramming) {
|
|
415
|
-
process.stdout.write(`技能:已装 ${installed}(默认 workflow ${displayTotal},含上游 ${displayUpstream};可分发 ${allSkillsFull.length},含上游 ${upstreamFull})\n`);
|
|
416
|
-
} else {
|
|
417
|
-
process.stdout.write(`技能:已装 ${installed}(可分发 ${displayTotal},含上游 ${displayUpstream})\n`);
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
|
-
process.stdout.write(`目标路径:${target}\n`);
|
|
421
|
-
}
|
|
422
|
-
async function copyDryRunInputs(target, stage) {
|
|
423
|
-
for (const relative of DRY_RUN_PATHS) {
|
|
424
|
-
const source = path.join(target, relative);
|
|
425
|
-
if (!await pathExists(source)) continue;
|
|
426
|
-
const destination = path.join(stage, relative);
|
|
427
|
-
await mkdir(path.dirname(destination), { recursive: true });
|
|
428
|
-
await cp(source, destination, { recursive: true, force: true });
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
async function compareDryRunTrees(target, stage) {
|
|
433
|
-
const result = { added: [], updated: [], removed: [] };
|
|
434
|
-
for (const relative of DRY_RUN_PATHS) {
|
|
435
|
-
const before = path.join(target, relative);
|
|
436
|
-
const after = path.join(stage, relative);
|
|
437
|
-
const beforeExists = await pathExists(before);
|
|
438
|
-
const afterExists = await pathExists(after);
|
|
439
|
-
if (!beforeExists && afterExists) result.added.push(relative);
|
|
440
|
-
else if (beforeExists && !afterExists) result.removed.push(relative);
|
|
441
|
-
else if (beforeExists && afterExists && !await sameTree(before, after)) result.updated.push(relative);
|
|
442
|
-
}
|
|
443
|
-
return result;
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
function formatTargetComparison({ target, result, onlyProgramming, refreshAgents }) {
|
|
447
|
-
const lines = [
|
|
448
|
-
`目标: ${target}`,
|
|
449
|
-
`范围: ${onlyProgramming ? '默认 workflow' : '全部可分发'}${refreshAgents ? ';刷新 AGENTS.md' : ''}`,
|
|
450
|
-
'',
|
|
451
|
-
];
|
|
452
|
-
const totalDiff = result.added.length + result.updated.length + result.removed.length;
|
|
453
|
-
if (totalDiff === 0) {
|
|
454
|
-
lines.push('✅ 无需更新');
|
|
455
|
-
return lines.join('\n');
|
|
456
|
-
}
|
|
457
|
-
if (result.added.length) lines.push(`新增 (${result.added.length}): ${result.added.join(', ')}`);
|
|
458
|
-
if (result.updated.length) lines.push(`更新 (${result.updated.length}): ${result.updated.join(', ')}`);
|
|
459
|
-
if (result.removed.length) lines.push(`删除 (${result.removed.length}): ${result.removed.join(', ')}`);
|
|
460
|
-
return lines.join('\n');
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
async function refreshAgentsFile(targetFile) {
|
|
464
|
-
const templateFile = path.join(TEMPLATE_DIR, 'AGENTS.md');
|
|
465
|
-
const current = await readFile(targetFile, 'utf8');
|
|
466
|
-
const template = await readFile(templateFile, 'utf8');
|
|
467
|
-
const merged = mergeManagedAgents(current, template);
|
|
468
|
-
if (merged !== null) {
|
|
469
|
-
if (merged !== current) await writeFile(targetFile, merged);
|
|
470
|
-
return 'managed';
|
|
471
|
-
}
|
|
472
|
-
await cp(targetFile, `${targetFile}.bak`, { force: true });
|
|
473
|
-
await cp(templateFile, targetFile, { force: true });
|
|
474
|
-
return 'full';
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
async function syncCommand({ dest, all, dryRun, json, refreshAgents, quiet = false }) {
|
|
478
|
-
const onlyProgramming = !all;
|
|
479
|
-
const output = (text) => {
|
|
480
|
-
if (!quiet) process.stdout.write(text);
|
|
481
|
-
};
|
|
482
|
-
if (!dryRun && json) throw new Error('--json 仅支持 sync --dry-run');
|
|
483
|
-
if (dryRun) {
|
|
484
|
-
const target = dest ? path.resolve(process.cwd(), dest) : process.cwd();
|
|
485
|
-
const stage = await mkdtemp(path.join(os.tmpdir(), 'matt-skills-sync-dry-run-'));
|
|
486
|
-
try {
|
|
487
|
-
await copyDryRunInputs(target, stage);
|
|
488
|
-
await syncCommand({ dest: stage, all, dryRun: false, json: false, refreshAgents, quiet: true });
|
|
489
|
-
const result = await compareDryRunTrees(target, stage);
|
|
490
|
-
const payload = { target, result, onlyProgramming, refreshAgents };
|
|
491
|
-
if (json) output(`${JSON.stringify(payload, null, 2)}\n`);
|
|
492
|
-
else output(`${formatTargetComparison(payload)}\n`);
|
|
493
|
-
const totalDiff = result.added.length + result.updated.length + result.removed.length;
|
|
494
|
-
if (totalDiff > 0) process.exitCode = 1;
|
|
495
|
-
} finally {
|
|
496
|
-
await rm(stage, { recursive: true, force: true });
|
|
497
|
-
}
|
|
498
|
-
return;
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
const target = dest ? path.resolve(process.cwd(), dest) : process.cwd();
|
|
502
|
-
const marker = path.join(target, 'AGENTS.md');
|
|
503
|
-
async function copyTemplateFiltered() {
|
|
504
|
-
await cp(TEMPLATE_DIR, target, {
|
|
505
|
-
recursive: true,
|
|
506
|
-
force: true,
|
|
507
|
-
filter: shouldCopyTemplatePath,
|
|
508
|
-
});
|
|
509
|
-
}
|
|
510
|
-
if (!(await pathExists(marker))) {
|
|
511
|
-
output('未检测到现有项目(AGENTS.md 不存在),将执行全新初始化\n');
|
|
512
|
-
await copyTemplateFiltered();
|
|
513
|
-
output(`模板:已复制(AGENTS.md、skills:${PROJECT_SKILL_DIRS})\n`);
|
|
514
|
-
} else {
|
|
515
|
-
output('同步:检测到现有项目,将增量更新\n');
|
|
516
|
-
let agentsManaged = false;
|
|
517
|
-
let agentsRefreshed = false;
|
|
518
|
-
let agentsRefreshMode = null;
|
|
519
|
-
if (refreshAgents) {
|
|
520
|
-
agentsRefreshMode = await refreshAgentsFile(marker);
|
|
521
|
-
agentsRefreshed = true;
|
|
522
|
-
} else {
|
|
523
|
-
try {
|
|
524
|
-
agentsManaged = await syncManagedAgents(marker);
|
|
525
|
-
} catch {}
|
|
526
|
-
}
|
|
527
|
-
await cp(path.join(TEMPLATE_DIR, '.opencode'), path.join(target, '.opencode'), { recursive: true, force: true });
|
|
528
|
-
await cp(path.join(TEMPLATE_DIR, '.pi'), path.join(target, '.pi'), { recursive: true, force: true });
|
|
529
|
-
if (agentsRefreshed && agentsRefreshMode === 'managed') {
|
|
530
|
-
output(`模板:已同步(AGENTS.md 受管区块已刷新、skills:${PROJECT_SKILL_DIRS},项目自定义内容已保留)\n`);
|
|
531
|
-
} else if (agentsRefreshed) {
|
|
532
|
-
output(`模板:已同步(AGENTS.md 已刷新,旧文件备份为 AGENTS.md.bak、skills:${PROJECT_SKILL_DIRS})\n`);
|
|
533
|
-
} else if (agentsManaged) {
|
|
534
|
-
output(`模板:已同步(AGENTS.md 受管区块已更新、skills:${PROJECT_SKILL_DIRS},项目自定义内容已保留)\n`);
|
|
535
|
-
} else {
|
|
536
|
-
output(`模板:已同步(AGENTS.md 未受管、skills:${PROJECT_SKILL_DIRS},已原样保留)\n`);
|
|
537
|
-
}
|
|
538
|
-
}
|
|
539
|
-
// --all 只扩大技能范围;同名覆盖、不存在新增,不删除目标中的额外技能。
|
|
540
|
-
const allSkills = await listSkillNames({ onlyProgramming });
|
|
541
|
-
const skillTargets = projectSkillTargets(target);
|
|
542
|
-
const skillsDir = path.resolve(target, PROJECT_DIRS.codex);
|
|
543
|
-
const legacyLocations = LEGACY_PROJECT_SKILL_DIRS.map((relative) => ({
|
|
544
|
-
dir: path.resolve(target, relative),
|
|
545
|
-
label: relative,
|
|
546
|
-
isWorkspaceSource: false,
|
|
547
|
-
}));
|
|
548
|
-
const preservedRepoLocal = [];
|
|
549
|
-
const preserveLocations = [
|
|
550
|
-
...skillTargets.map(({ tool, dir }) => ({
|
|
551
|
-
dir,
|
|
552
|
-
label: PROJECT_DIRS[tool],
|
|
553
|
-
isWorkspaceSource: path.resolve(dir) === path.resolve(SKILLS_DIR),
|
|
554
|
-
})),
|
|
555
|
-
...legacyLocations,
|
|
556
|
-
];
|
|
557
|
-
for (const { dir } of skillTargets) await mkdir(dir, { recursive: true });
|
|
558
|
-
for (const name of REPO_LOCAL_SKILLS) {
|
|
559
|
-
for (const location of preserveLocations) {
|
|
560
|
-
if (!location.isWorkspaceSource && await pathExists(path.join(location.dir, name))) {
|
|
561
|
-
preservedRepoLocal.push(`${location.label}/${name}`);
|
|
562
|
-
}
|
|
563
|
-
}
|
|
564
|
-
}
|
|
565
|
-
let installed = 0;
|
|
566
|
-
let updated = 0;
|
|
567
|
-
for (const { dir } of skillTargets) {
|
|
568
|
-
for (const name of allSkills) {
|
|
569
|
-
const src = path.join(SKILLS_DIR, name);
|
|
570
|
-
const dst = path.join(dir, name);
|
|
571
|
-
if (path.resolve(src) === path.resolve(dst)) {
|
|
572
|
-
if (path.resolve(dir) === skillsDir) updated++;
|
|
573
|
-
continue;
|
|
574
|
-
}
|
|
575
|
-
const exists = await pathExists(dst);
|
|
576
|
-
if (exists) {
|
|
577
|
-
await rm(dst, { recursive: true, force: true });
|
|
578
|
-
await cp(src, dst, { recursive: true, force: true });
|
|
579
|
-
if (path.resolve(dir) === skillsDir) updated++;
|
|
580
|
-
} else {
|
|
581
|
-
await cp(src, dst, { recursive: true, force: true });
|
|
582
|
-
if (path.resolve(dir) === skillsDir) installed++;
|
|
583
|
-
}
|
|
584
|
-
}
|
|
585
|
-
}
|
|
586
|
-
const distributableSkillNames = (await listSkillNames({ onlyProgramming: false })).filter((name) => !REPO_LOCAL_SKILLS.has(name));
|
|
587
|
-
const cleanedLegacy = [];
|
|
588
|
-
for (const location of legacyLocations) {
|
|
589
|
-
let entries;
|
|
590
|
-
try {
|
|
591
|
-
if (!await isSafeRealPath(location.dir)) continue;
|
|
592
|
-
const locationInfo = await lstat(location.dir);
|
|
593
|
-
if (!locationInfo.isDirectory()) continue;
|
|
594
|
-
entries = await readdir(location.dir, { withFileTypes: true });
|
|
595
|
-
} catch {
|
|
596
|
-
continue;
|
|
597
|
-
}
|
|
598
|
-
for (const entry of entries) {
|
|
599
|
-
if (!entry.isDirectory() || entry.name === '.git' || entry.name.endsWith('.bak')) continue;
|
|
600
|
-
if (!distributableSkillNames.includes(entry.name)) continue;
|
|
601
|
-
const src = path.join(SKILLS_DIR, entry.name);
|
|
602
|
-
const dst = path.join(location.dir, entry.name);
|
|
603
|
-
try {
|
|
604
|
-
if (await sameTree(src, dst)) {
|
|
605
|
-
await rm(dst, { recursive: true, force: true });
|
|
606
|
-
cleanedLegacy.push(`${location.label}/${entry.name}`);
|
|
607
|
-
}
|
|
608
|
-
} catch {}
|
|
609
|
-
}
|
|
610
|
-
}
|
|
611
|
-
// Legacy harness dirs may contain project-local Skills. Only exact canonical
|
|
612
|
-
// trees of clearly distributable skills are removed; repo-local, modified or
|
|
613
|
-
// unknown trees remain untouched.
|
|
614
|
-
// 清理过时的 .pi/settings.json 指向
|
|
615
|
-
try {
|
|
616
|
-
const piSettings = path.join(target, '.pi/settings.json');
|
|
617
|
-
if (await pathExists(piSettings)) {
|
|
618
|
-
const txt = await readFile(piSettings, 'utf8');
|
|
619
|
-
if (txt.includes('.opencode/skills') || txt.includes('../.opencode')) {
|
|
620
|
-
await writeFile(piSettings, '{}\n');
|
|
621
|
-
}
|
|
622
|
-
}
|
|
623
|
-
} catch {}
|
|
624
|
-
if (cleanedLegacy.length) {
|
|
625
|
-
output(`迁移清理:${cleanedLegacy.join(', ')} 旧共享副本已移除\n`);
|
|
626
|
-
}
|
|
627
|
-
if (preservedRepoLocal.length) {
|
|
628
|
-
output(`迁移提示:${preservedRepoLocal.join(', ')} 已不再分发,现有副本已保留\n`);
|
|
629
|
-
}
|
|
630
|
-
const modeLabel = onlyProgramming ? '默认 workflow' : '全部可分发';
|
|
631
|
-
output(`技能:新增 ${installed}、更新 ${updated}(${modeLabel} ${allSkills.length})\n`);
|
|
632
|
-
output(`目标路径:${target}\n`);
|
|
633
|
-
}
|
|
634
|
-
|
|
635
|
-
function parseInitArgs(args) {
|
|
636
|
-
let dest;
|
|
637
|
-
let all = false;
|
|
638
|
-
for (let i = 0; i < args.length; i++) {
|
|
639
|
-
const arg = args[i];
|
|
640
|
-
if (arg === '--dest') {
|
|
641
|
-
if (i + 1 >= args.length || args[i + 1].startsWith('-')) throw new Error(`unknown option '--dest' requires a value`);
|
|
642
|
-
dest = args[++i];
|
|
643
|
-
} else if (arg.startsWith('--dest=')) dest = arg.slice('--dest='.length);
|
|
644
|
-
else if (arg === '--all') all = true;
|
|
645
|
-
else if (arg === '--help' || arg === '-h') {} // handled at main level, ignore here
|
|
646
|
-
else if (arg.startsWith('-')) throw new Error(`unknown option '${arg}' for command 'init'`);
|
|
647
|
-
else throw new Error(`unknown argument '${arg}' for command 'init'`);
|
|
648
|
-
}
|
|
649
|
-
return { dest, all };
|
|
650
|
-
}
|
|
651
|
-
|
|
652
|
-
function parseSyncArgs(args) {
|
|
653
|
-
let dest;
|
|
654
|
-
let all = false;
|
|
655
|
-
let dryRun = false;
|
|
656
|
-
let json = false;
|
|
657
|
-
let refreshAgents = false;
|
|
658
|
-
for (let i = 0; i < args.length; i++) {
|
|
659
|
-
const arg = args[i];
|
|
660
|
-
if (arg === '--dest') {
|
|
661
|
-
if (i + 1 >= args.length || args[i + 1].startsWith('-')) throw new Error(`unknown option '--dest' requires a value`);
|
|
662
|
-
dest = args[++i];
|
|
663
|
-
} else if (arg.startsWith('--dest=')) dest = arg.slice('--dest='.length);
|
|
664
|
-
else if (arg === '--all') all = true;
|
|
665
|
-
else if (arg === '--dry-run') dryRun = true;
|
|
666
|
-
else if (arg === '--json') json = true;
|
|
667
|
-
else if (arg === '--refresh-agents') refreshAgents = true;
|
|
668
|
-
else if (arg === '--help' || arg === '-h') {} // handled at main
|
|
669
|
-
else if (arg === '--apply') {} // deprecated alias, same as default safe incremental
|
|
670
|
-
else if (arg.startsWith('-')) throw new Error(`unknown option '${arg}' for command 'sync'`);
|
|
671
|
-
else throw new Error(`unknown argument '${arg}' for command 'sync'`);
|
|
672
|
-
}
|
|
673
|
-
return { dest, all, dryRun, json, refreshAgents };
|
|
674
|
-
}
|
|
675
|
-
|
|
676
|
-
function parseInstallArgs(args) {
|
|
677
|
-
let dest;
|
|
678
|
-
let all = false;
|
|
679
|
-
let force = false;
|
|
680
|
-
let global = false;
|
|
681
|
-
let toolsArg;
|
|
682
|
-
for (let i = 0; i < args.length; i++) {
|
|
683
|
-
const arg = args[i];
|
|
684
|
-
if (arg === '--dest') {
|
|
685
|
-
if (i + 1 >= args.length || args[i + 1].startsWith('-')) throw new Error(`unknown option '--dest' requires a value`);
|
|
686
|
-
dest = args[++i];
|
|
687
|
-
} else if (arg.startsWith('--dest=')) dest = arg.slice('--dest='.length);
|
|
688
|
-
else if (arg === '--all') all = true;
|
|
689
|
-
else if (arg === '--force') force = true;
|
|
690
|
-
else if (arg === '--tools') {
|
|
691
|
-
if (i + 1 >= args.length || args[i + 1].startsWith('-')) throw new Error(`unknown option '--tools' requires a value`);
|
|
692
|
-
toolsArg = args[++i];
|
|
693
|
-
} else if (arg.startsWith('--tools=')) toolsArg = arg.slice('--tools='.length);
|
|
694
|
-
else if (arg === '--global') global = true;
|
|
695
|
-
else if (arg === '--project') global = false;
|
|
696
|
-
else if (arg === '--help' || arg === '-h') {} // handled at main
|
|
697
|
-
else if (arg.startsWith('-')) throw new Error(`unknown option '${arg}' for command 'install'`);
|
|
698
|
-
else throw new Error(`unknown argument '${arg}' for command 'install'`);
|
|
699
|
-
}
|
|
700
|
-
const tools = toolsArg
|
|
701
|
-
? toolsArg.split(',').map((t) => t.trim()).filter(Boolean)
|
|
702
|
-
: null;
|
|
703
|
-
return { dest, all, force, global, tools };
|
|
704
|
-
}
|
|
705
|
-
|
|
706
|
-
async function checkCommand(args) {
|
|
707
|
-
const { compare, formatComparison } = await import('../scripts/sync-upstream.js');
|
|
708
|
-
const json = args.includes('--json');
|
|
709
|
-
const onlyProgramming = !args.includes('--all');
|
|
710
|
-
const upstreamIdx = args.indexOf('--upstream');
|
|
711
|
-
const upstreamEq = args.find((x) => x.startsWith('--upstream='));
|
|
712
|
-
const upstreamUrl = upstreamIdx !== -1 ? args[upstreamIdx + 1] : (upstreamEq ? upstreamEq.slice('--upstream='.length) : undefined);
|
|
713
|
-
const refIdx = args.indexOf('--ref');
|
|
714
|
-
const refEq = args.find((x) => x.startsWith('--ref='));
|
|
715
|
-
const ref = refIdx !== -1 ? args[refIdx + 1] : (refEq ? refEq.slice('--ref='.length) : undefined);
|
|
716
|
-
const cmp = await compare({ upstreamUrl, ref, onlyProgramming });
|
|
717
|
-
if (json) {
|
|
718
|
-
process.stdout.write(JSON.stringify({ head: cmp.head, counts: cmp.counts, result: cmp.result, onlyProgramming }, null, 2) + '\n');
|
|
719
|
-
} else {
|
|
720
|
-
process.stdout.write(formatComparison(cmp) + '\n');
|
|
721
|
-
}
|
|
722
|
-
const { rm } = await import('node:fs/promises');
|
|
723
|
-
await rm(cmp.dest, { recursive: true, force: true });
|
|
724
|
-
const hasDiff = cmp.result.added.length + cmp.result.updated.length + cmp.result.removed.length + cmp.result.renamed.length > 0;
|
|
725
|
-
if (hasDiff) process.exitCode = 1;
|
|
726
|
-
}
|
|
727
109
|
|
|
728
110
|
async function main() {
|
|
729
111
|
const args = process.argv.slice(2);
|
|
@@ -764,55 +146,23 @@ async function main() {
|
|
|
764
146
|
return;
|
|
765
147
|
}
|
|
766
148
|
if (command === 'list') {
|
|
767
|
-
|
|
768
|
-
for (const a of rest) {
|
|
769
|
-
if (a === '--all' || a === '--json' || a === '--help' || a === '-h') continue;
|
|
770
|
-
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; }
|
|
771
|
-
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;
|
|
772
|
-
}
|
|
773
|
-
const onlyProgramming = !rest.includes('--all');
|
|
774
|
-
const skills = await listSkills({ onlyProgramming });
|
|
775
|
-
if (rest.includes('--json')) {
|
|
776
|
-
process.stdout.write(`${JSON.stringify(skills, null, 2)}\n`);
|
|
777
|
-
} else {
|
|
778
|
-
for (const skill of skills) {
|
|
779
|
-
process.stdout.write(`${skill.name} — ${skill.description}\n`);
|
|
780
|
-
}
|
|
781
|
-
}
|
|
149
|
+
await runList(rest);
|
|
782
150
|
return;
|
|
783
151
|
}
|
|
784
152
|
if (command === 'init') {
|
|
785
|
-
await
|
|
153
|
+
await runInit(rest);
|
|
786
154
|
return;
|
|
787
155
|
}
|
|
788
156
|
if (command === 'sync') {
|
|
789
|
-
await
|
|
157
|
+
await runSync(rest);
|
|
790
158
|
return;
|
|
791
159
|
}
|
|
792
160
|
if (command === 'install') {
|
|
793
|
-
await
|
|
161
|
+
await runInstall(rest);
|
|
794
162
|
return;
|
|
795
163
|
}
|
|
796
164
|
if (command === 'check') {
|
|
797
|
-
|
|
798
|
-
for (let i = 0; i < rest.length; i++) {
|
|
799
|
-
const a = rest[i];
|
|
800
|
-
if (a === '--all' || a === '--json' || a === '--help' || a === '-h') continue;
|
|
801
|
-
if (a === '--upstream' || a === '--ref') {
|
|
802
|
-
if (i + 1 >= rest.length || rest[i + 1].startsWith('-')) {
|
|
803
|
-
process.stderr.write(`error: option '${a}' for command 'check' requires a value\n`);
|
|
804
|
-
process.stderr.write(`Run 'matt-skills check --help' for usage.\n`);
|
|
805
|
-
process.exitCode = 2;
|
|
806
|
-
return;
|
|
807
|
-
}
|
|
808
|
-
i++;
|
|
809
|
-
continue;
|
|
810
|
-
}
|
|
811
|
-
if (a.startsWith('--upstream=') || a.startsWith('--ref=')) continue;
|
|
812
|
-
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; }
|
|
813
|
-
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;
|
|
814
|
-
}
|
|
815
|
-
await checkCommand(rest);
|
|
165
|
+
await runCheck(rest);
|
|
816
166
|
return;
|
|
817
167
|
}
|
|
818
168
|
if (command === 'update') {
|