@heihei0299/matt-skills 3.0.0 → 3.0.2
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/bin/cli.js +24 -72
- package/bin/skill-boundaries.js +0 -8
- package/bin/skill-config.js +21 -0
- package/bin/skill-selection.js +27 -0
- package/config/required.json +5 -0
- package/package.json +2 -1
- package/scripts/sync-upstream.js +7 -16
package/bin/cli.js
CHANGED
|
@@ -6,12 +6,12 @@ import { fileURLToPath } from 'node:url';
|
|
|
6
6
|
import prompts from 'prompts';
|
|
7
7
|
import {
|
|
8
8
|
PROPRIETARY_SKILLS,
|
|
9
|
-
isDefaultProgrammingSkill,
|
|
10
|
-
isDistributableProprietarySkill,
|
|
11
9
|
isDistributableSkill,
|
|
12
10
|
isRepoLocalSkill,
|
|
13
11
|
REPO_LOCAL_SKILLS,
|
|
14
12
|
} from './skill-boundaries.js';
|
|
13
|
+
import { resolveSkillNames } from './skill-selection.js';
|
|
14
|
+
import { loadSkillSet } from './skill-config.js';
|
|
15
15
|
|
|
16
16
|
const SKILLS_DIR = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', '.agents', 'skills');
|
|
17
17
|
const TEMPLATE_DIR = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'template');
|
|
@@ -19,32 +19,15 @@ const ENGINEERING_PATH = path.join(path.dirname(fileURLToPath(import.meta.url)),
|
|
|
19
19
|
const REQUIRED_PATH = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'config', 'required.json');
|
|
20
20
|
let ENGINEERING_SKILLS = null;
|
|
21
21
|
async function loadEngineeringSkills() {
|
|
22
|
-
if (ENGINEERING_SKILLS)
|
|
23
|
-
try {
|
|
24
|
-
const raw = await readFile(ENGINEERING_PATH, 'utf8');
|
|
25
|
-
ENGINEERING_SKILLS = new Set(JSON.parse(raw));
|
|
26
|
-
} catch {
|
|
27
|
-
ENGINEERING_SKILLS = new Set(['ask-matt','code-review','codebase-design','diagnosing-bugs','domain-modeling','grill-with-docs','implement','improve-codebase-architecture','prototype','research','resolving-merge-conflicts','setup-matt-pocock-skills','tdd','to-spec','to-tickets','triage','wayfinder','wizard']);
|
|
28
|
-
}
|
|
22
|
+
if (!ENGINEERING_SKILLS) ENGINEERING_SKILLS = await loadSkillSet(ENGINEERING_PATH, 'engineering');
|
|
29
23
|
return ENGINEERING_SKILLS;
|
|
30
24
|
}
|
|
25
|
+
|
|
31
26
|
let REQUIRED_SKILLS = null;
|
|
32
27
|
async function loadRequiredSkills() {
|
|
33
|
-
if (REQUIRED_SKILLS)
|
|
34
|
-
try {
|
|
35
|
-
const raw = await readFile(REQUIRED_PATH, 'utf8');
|
|
36
|
-
REQUIRED_SKILLS = new Set(JSON.parse(raw));
|
|
37
|
-
} catch {
|
|
38
|
-
REQUIRED_SKILLS = new Set(['grilling', 'grill-me', 'handoff']);
|
|
39
|
-
}
|
|
28
|
+
if (!REQUIRED_SKILLS) REQUIRED_SKILLS = await loadSkillSet(REQUIRED_PATH, 'required');
|
|
40
29
|
return REQUIRED_SKILLS;
|
|
41
30
|
}
|
|
42
|
-
function isProgrammingSkill(name, engineering, required) {
|
|
43
|
-
return isDefaultProgrammingSkill(name, engineering, required);
|
|
44
|
-
}
|
|
45
|
-
function isProgrammingAll(name, engineering) {
|
|
46
|
-
return isDistributableProprietarySkill(name) || engineering.has(name);
|
|
47
|
-
}
|
|
48
31
|
process.stdout.on('error', (err) => {
|
|
49
32
|
if (err.code === 'EPIPE') process.exit(0);
|
|
50
33
|
throw err;
|
|
@@ -156,20 +139,22 @@ function parseFrontmatter(text) {
|
|
|
156
139
|
return fields;
|
|
157
140
|
}
|
|
158
141
|
|
|
159
|
-
async function
|
|
142
|
+
async function listAvailableSkillNames() {
|
|
160
143
|
const entries = await readdir(SKILLS_DIR, { withFileTypes: true });
|
|
161
|
-
|
|
144
|
+
return entries
|
|
162
145
|
.filter((entry) => entry.isDirectory())
|
|
163
146
|
.filter((entry) => !entry.name.endsWith('.bak'))
|
|
164
147
|
.filter((entry) => entry.name !== 'skill-creator' && entry.name !== '.git')
|
|
165
|
-
.filter((entry) => !isRepoLocalSkill(entry.name))
|
|
166
148
|
.map((entry) => entry.name);
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async function listSkillNames({ onlyProgramming = false } = {}) {
|
|
152
|
+
return resolveSkillNames({
|
|
153
|
+
availableNames: await listAvailableSkillNames(),
|
|
154
|
+
mode: onlyProgramming ? 'default' : 'all',
|
|
155
|
+
engineering: onlyProgramming ? await loadEngineeringSkills() : [],
|
|
156
|
+
required: onlyProgramming ? await loadRequiredSkills() : [],
|
|
157
|
+
});
|
|
173
158
|
}
|
|
174
159
|
|
|
175
160
|
async function listSkills({ onlyProgramming = false } = {}) {
|
|
@@ -249,11 +234,8 @@ async function promptSkills(skills) {
|
|
|
249
234
|
|
|
250
235
|
async function installCommand({ dest, all, force, tools, global }) {
|
|
251
236
|
const onlyProgramming = !all;
|
|
252
|
-
const engineering = onlyProgramming ? await loadEngineeringSkills() : null;
|
|
253
|
-
const required = onlyProgramming ? await loadRequiredSkills() : null;
|
|
254
237
|
const skillNames = await listSkillNames({ onlyProgramming });
|
|
255
|
-
const
|
|
256
|
-
const skills = onlyProgramming ? skillsAll.filter(s => isProgrammingSkill(s.name, engineering, required)) : skillsAll;
|
|
238
|
+
const skills = await listSkills({ onlyProgramming });
|
|
257
239
|
let targets;
|
|
258
240
|
if (dest) {
|
|
259
241
|
targets = [{ tool: null, dir: path.resolve(process.cwd(), dest) }];
|
|
@@ -338,11 +320,10 @@ async function initCommand({ dest, all }) {
|
|
|
338
320
|
installed = entries.filter((e) => e.isDirectory() && !e.name.endsWith('.bak') && e.name !== '.git' && e.name !== 'skill-creator' && !isRepoLocalSkill(e.name)).length;
|
|
339
321
|
} catch {}
|
|
340
322
|
const allSkillsFull = await listSkills({ onlyProgramming: false });
|
|
341
|
-
const
|
|
342
|
-
const
|
|
343
|
-
const programmingCount = allSkillsFull.filter(s => isProgrammingSkill(s.name, engineeringForStats, requiredForStats)).length;
|
|
323
|
+
const programmingSkills = await listSkills({ onlyProgramming: true });
|
|
324
|
+
const programmingCount = programmingSkills.length;
|
|
344
325
|
const upstreamFull = allSkillsFull.filter((s) => !PROPRIETARY_SKILLS.has(s.name)).length;
|
|
345
|
-
const upstreamProg =
|
|
326
|
+
const upstreamProg = programmingSkills.filter((s) => !PROPRIETARY_SKILLS.has(s.name)).length;
|
|
346
327
|
const displayTotal = onlyProgramming ? programmingCount : allSkillsFull.length;
|
|
347
328
|
const displayUpstream = onlyProgramming ? upstreamProg : upstreamFull;
|
|
348
329
|
if (path.resolve(skillsDir) === path.resolve(SKILLS_DIR)) {
|
|
@@ -359,26 +340,12 @@ async function initCommand({ dest, all }) {
|
|
|
359
340
|
async function syncCommand({ dest, all, dryRun, json, upstreamUrl, ref }) {
|
|
360
341
|
const onlyProgramming = !all;
|
|
361
342
|
if (dryRun) {
|
|
362
|
-
const { compare } = await import('../scripts/sync-upstream.js');
|
|
343
|
+
const { compare, formatComparison } = await import('../scripts/sync-upstream.js');
|
|
363
344
|
const cmp = await compare({ upstreamUrl, ref, onlyProgramming });
|
|
364
345
|
if (json) {
|
|
365
346
|
process.stdout.write(JSON.stringify({ head: cmp.head, counts: cmp.counts, result: cmp.result, onlyProgramming }, null, 2) + '\n');
|
|
366
347
|
} else {
|
|
367
|
-
|
|
368
|
-
const lines = [];
|
|
369
|
-
lines.push(`上游 HEAD: ${cmp.head}`);
|
|
370
|
-
lines.push(`本地非独有: ${cmp.counts.local} 上游: ${cmp.counts.upstream} ${modeHint}`);
|
|
371
|
-
lines.push('');
|
|
372
|
-
const totalDiff = cmp.result.added.length + cmp.result.updated.length + cmp.result.removed.length + cmp.result.renamed.length;
|
|
373
|
-
if (totalDiff === 0) lines.push('✅ 已是最新,无差异');
|
|
374
|
-
else {
|
|
375
|
-
if (cmp.result.added.length) lines.push(`新增 (${cmp.result.added.length}): ${cmp.result.added.join(', ')}`);
|
|
376
|
-
if (cmp.result.updated.length) lines.push(`更新 (${cmp.result.updated.length}): ${cmp.result.updated.join(', ')}`);
|
|
377
|
-
if (cmp.result.renamed.length) lines.push(`重命名 (${cmp.result.renamed.length}): ${cmp.result.renamed.map((r) => `${r.from}→${r.to}`).join(', ')}`);
|
|
378
|
-
if (cmp.result.removed.length) lines.push(`删除 (${cmp.result.removed.length}): ${cmp.result.removed.join(', ')}`);
|
|
379
|
-
if (cmp.result.same.length) lines.push(`一致 (${cmp.result.same.length}): ${cmp.result.same.join(', ')}`);
|
|
380
|
-
}
|
|
381
|
-
process.stdout.write(lines.join('\n') + '\n');
|
|
348
|
+
process.stdout.write(formatComparison(cmp) + '\n');
|
|
382
349
|
}
|
|
383
350
|
const { rm } = await import('node:fs/promises');
|
|
384
351
|
await rm(cmp.dest, { recursive: true, force: true });
|
|
@@ -578,7 +545,7 @@ function parseInstallArgs(args) {
|
|
|
578
545
|
}
|
|
579
546
|
|
|
580
547
|
async function checkCommand(args) {
|
|
581
|
-
const { compare } = await import('../scripts/sync-upstream.js');
|
|
548
|
+
const { compare, formatComparison } = await import('../scripts/sync-upstream.js');
|
|
582
549
|
const json = args.includes('--json');
|
|
583
550
|
const onlyProgramming = !args.includes('--all');
|
|
584
551
|
const upstreamIdx = args.indexOf('--upstream');
|
|
@@ -591,22 +558,7 @@ async function checkCommand(args) {
|
|
|
591
558
|
if (json) {
|
|
592
559
|
process.stdout.write(JSON.stringify({ head: cmp.head, counts: cmp.counts, result: cmp.result, onlyProgramming }, null, 2) + '\n');
|
|
593
560
|
} else {
|
|
594
|
-
|
|
595
|
-
lines.push(`上游 HEAD: ${cmp.head}`);
|
|
596
|
-
const modeHint = onlyProgramming ? '(默认:engineering + 独有所需)' : '(全量上游)';
|
|
597
|
-
lines.push(`本地非独有: ${cmp.counts.local} 上游: ${cmp.counts.upstream} ${modeHint}`);
|
|
598
|
-
lines.push('');
|
|
599
|
-
const totalDiff = cmp.result.added.length + cmp.result.updated.length + cmp.result.removed.length + cmp.result.renamed.length;
|
|
600
|
-
if (totalDiff === 0) {
|
|
601
|
-
lines.push('✅ 已是最新,无差异');
|
|
602
|
-
} else {
|
|
603
|
-
if (cmp.result.added.length) lines.push(`新增 (${cmp.result.added.length}): ${cmp.result.added.join(', ')}`);
|
|
604
|
-
if (cmp.result.updated.length) lines.push(`更新 (${cmp.result.updated.length}): ${cmp.result.updated.join(', ')}`);
|
|
605
|
-
if (cmp.result.renamed.length) lines.push(`重命名 (${cmp.result.renamed.length}): ${cmp.result.renamed.map((r) => `${r.from}→${r.to}`).join(', ')}`);
|
|
606
|
-
if (cmp.result.removed.length) lines.push(`删除 (${cmp.result.removed.length}): ${cmp.result.removed.join(', ')}`);
|
|
607
|
-
if (cmp.result.same.length) lines.push(`一致 (${cmp.result.same.length}): ${cmp.result.same.join(', ')}`);
|
|
608
|
-
}
|
|
609
|
-
process.stdout.write(lines.join('\n') + '\n');
|
|
561
|
+
process.stdout.write(formatComparison(cmp) + '\n');
|
|
610
562
|
}
|
|
611
563
|
const { rm } = await import('node:fs/promises');
|
|
612
564
|
await rm(cmp.dest, { recursive: true, force: true });
|
package/bin/skill-boundaries.js
CHANGED
|
@@ -46,14 +46,6 @@ export function isDefaultProprietarySkill(name) {
|
|
|
46
46
|
return DEFAULT_PROPRIETARY_SKILLS.has(name);
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
export function isDefaultProgrammingSkill(name, engineering, required) {
|
|
50
|
-
return (
|
|
51
|
-
DEFAULT_PROPRIETARY_SKILLS.has(name) ||
|
|
52
|
-
engineering.has(name) ||
|
|
53
|
-
(required && required.has(name))
|
|
54
|
-
);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
49
|
export function isDistributableSkill(name, knownNames) {
|
|
58
50
|
if (!knownNames) return isDistributableProprietarySkill(name);
|
|
59
51
|
const known = knownNames instanceof Set ? knownNames : new Set(knownNames);
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
|
|
3
|
+
export async function loadSkillSet(file, label) {
|
|
4
|
+
let raw;
|
|
5
|
+
try {
|
|
6
|
+
raw = await readFile(file, 'utf8');
|
|
7
|
+
} catch (error) {
|
|
8
|
+
throw new Error(`unable to read ${label} skill config: ${error.message}`);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
let value;
|
|
12
|
+
try {
|
|
13
|
+
value = JSON.parse(raw);
|
|
14
|
+
} catch (error) {
|
|
15
|
+
throw new Error(`invalid ${label} skill config: ${error.message}`);
|
|
16
|
+
}
|
|
17
|
+
if (!Array.isArray(value) || value.some((name) => typeof name !== 'string')) {
|
|
18
|
+
throw new Error(`invalid ${label} skill config: expected an array of strings`);
|
|
19
|
+
}
|
|
20
|
+
return new Set(value);
|
|
21
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DEFAULT_PROPRIETARY_SKILLS,
|
|
3
|
+
isDistributableSkill,
|
|
4
|
+
} from './skill-boundaries.js';
|
|
5
|
+
|
|
6
|
+
function asSet(value) {
|
|
7
|
+
return value instanceof Set ? new Set(value) : new Set(value ?? []);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function resolveSkillNames({ availableNames, mode = 'default', engineering, required }) {
|
|
11
|
+
if (mode !== 'default' && mode !== 'all') {
|
|
12
|
+
throw new Error(`unknown skill selection mode: ${mode}`);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const available = asSet(availableNames);
|
|
16
|
+
const candidates = mode === 'all'
|
|
17
|
+
? available
|
|
18
|
+
: new Set([
|
|
19
|
+
...asSet(engineering),
|
|
20
|
+
...asSet(required),
|
|
21
|
+
...DEFAULT_PROPRIETARY_SKILLS,
|
|
22
|
+
]);
|
|
23
|
+
|
|
24
|
+
return [...candidates]
|
|
25
|
+
.filter((name) => available.has(name) && isDistributableSkill(name, available))
|
|
26
|
+
.sort();
|
|
27
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@heihei0299/matt-skills",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.2",
|
|
4
4
|
"description": "Agent skills + 项目配置模板:一条命令初始化 opencode / pi-agent 项目(含 mattpocock/skills 上游技能)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
"scripts/codex-smoke.js",
|
|
15
15
|
"config/proprietary.json",
|
|
16
16
|
"config/engineering.json",
|
|
17
|
+
"config/required.json",
|
|
17
18
|
"README.md"
|
|
18
19
|
],
|
|
19
20
|
"scripts": {
|
package/scripts/sync-upstream.js
CHANGED
|
@@ -6,6 +6,7 @@ import path from 'node:path';
|
|
|
6
6
|
import os from 'node:os';
|
|
7
7
|
import { fileURLToPath } from 'node:url';
|
|
8
8
|
import { PROPRIETARY_SKILLS } from '../bin/skill-boundaries.js';
|
|
9
|
+
import { loadSkillSet } from '../bin/skill-config.js';
|
|
9
10
|
|
|
10
11
|
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
11
12
|
const LOCAL_SKILLS_DIR = path.join(ROOT, '.agents', 'skills');
|
|
@@ -23,20 +24,10 @@ async function loadProprietary() {
|
|
|
23
24
|
}
|
|
24
25
|
|
|
25
26
|
async function loadEngineering() {
|
|
26
|
-
|
|
27
|
-
const raw = await readFile(ENGINEERING_PATH, 'utf8');
|
|
28
|
-
return new Set(JSON.parse(raw));
|
|
29
|
-
} catch {
|
|
30
|
-
return new Set(['ask-matt','code-review','codebase-design','diagnosing-bugs','domain-modeling','grill-with-docs','implement','improve-codebase-architecture','prototype','research','resolving-merge-conflicts','setup-matt-pocock-skills','tdd','to-spec','to-tickets','triage','wayfinder','wizard']);
|
|
31
|
-
}
|
|
27
|
+
return loadSkillSet(ENGINEERING_PATH, 'engineering');
|
|
32
28
|
}
|
|
33
29
|
async function loadRequired() {
|
|
34
|
-
|
|
35
|
-
const raw = await readFile(REQUIRED_PATH, 'utf8');
|
|
36
|
-
return new Set(JSON.parse(raw));
|
|
37
|
-
} catch {
|
|
38
|
-
return new Set(['grilling', 'grill-me', 'handoff']);
|
|
39
|
-
}
|
|
30
|
+
return loadSkillSet(REQUIRED_PATH, 'required');
|
|
40
31
|
}
|
|
41
32
|
|
|
42
33
|
async function hashFile(filePath) {
|
|
@@ -120,12 +111,12 @@ async function collectLocalSkills(proprietary) {
|
|
|
120
111
|
export async function compare({ upstreamUrl, tmpDir, ref, onlyProgramming = true } = {}) {
|
|
121
112
|
const proprietary = await loadProprietary();
|
|
122
113
|
const engineering = await loadEngineering();
|
|
114
|
+
const required = await loadRequired();
|
|
123
115
|
const fetched = await fetchUpstream({ tmpDir, upstreamUrl, ref });
|
|
124
116
|
const upstreamRoot = fetched.dest;
|
|
125
117
|
const upstreamMapFull = await collectUpstreamSkills(upstreamRoot);
|
|
126
118
|
const localMapFull = await collectLocalSkills(proprietary);
|
|
127
119
|
// 默认范围:engineering 桶(编程)+ 独有所需(config/required.json,如 grill-to-spec 经 grill-with-docs 所需的 grilling);--all 则含全部 productivity
|
|
128
|
-
const required = await loadRequired();
|
|
129
120
|
const upstreamMap = onlyProgramming
|
|
130
121
|
? new Map([...upstreamMapFull.entries()].filter(([name, v]) => v.bucket === 'engineering' || required.has(name)))
|
|
131
122
|
: upstreamMapFull;
|
|
@@ -251,7 +242,7 @@ export async function applySync({ upstreamUrl, tmpDir, ref, dryRun = false, forc
|
|
|
251
242
|
|
|
252
243
|
return { ...cmp, dest: null, actions, head };
|
|
253
244
|
}
|
|
254
|
-
function
|
|
245
|
+
export function formatComparison(cmp) {
|
|
255
246
|
const { result, counts, head, onlyProgramming } = cmp;
|
|
256
247
|
const lines = [];
|
|
257
248
|
lines.push(`上游 HEAD: ${head}`);
|
|
@@ -316,7 +307,7 @@ Options:
|
|
|
316
307
|
if (opts.json) {
|
|
317
308
|
process.stdout.write(JSON.stringify({ head: res.head, result: res.result, actions: res.actions, dryRun: opts.dryRun, onlyProgramming }, null, 2) + '\n');
|
|
318
309
|
} else {
|
|
319
|
-
process.stdout.write(
|
|
310
|
+
process.stdout.write(formatComparison(res) + '\n');
|
|
320
311
|
if (res.actions.length) {
|
|
321
312
|
process.stdout.write(`\n已执行 ${res.actions.length} 项:\n`);
|
|
322
313
|
for (const a of res.actions) process.stdout.write(` - ${a}\n`);
|
|
@@ -333,7 +324,7 @@ Options:
|
|
|
333
324
|
if (opts.json) {
|
|
334
325
|
process.stdout.write(JSON.stringify({ head: cmp.head, counts: cmp.counts, result: cmp.result, onlyProgramming }, null, 2) + '\n');
|
|
335
326
|
} else {
|
|
336
|
-
process.stdout.write(
|
|
327
|
+
process.stdout.write(formatComparison(cmp) + '\n');
|
|
337
328
|
}
|
|
338
329
|
const hasDiff = cmp.result.added.length + cmp.result.updated.length + cmp.result.removed.length + cmp.result.renamed.length > 0;
|
|
339
330
|
// 清理临时目录
|