@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 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) return 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) return 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 listSkillNames({ onlyProgramming = false } = {}) {
142
+ async function listAvailableSkillNames() {
160
143
  const entries = await readdir(SKILLS_DIR, { withFileTypes: true });
161
- let names = entries
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
- if (onlyProgramming) {
168
- const engineering = await loadEngineeringSkills();
169
- const required = await loadRequiredSkills();
170
- names = names.filter((name) => isProgrammingSkill(name, engineering, required));
171
- }
172
- return names.sort();
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 skillsAll = await listSkills({ onlyProgramming: false });
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 engineeringForStats = await loadEngineeringSkills();
342
- const requiredForStats = await loadRequiredSkills();
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 = allSkillsFull.filter((s) => !PROPRIETARY_SKILLS.has(s.name) && (engineeringForStats.has(s.name) || requiredForStats.has(s.name))).length;
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
- const modeHint = onlyProgramming ? '(默认:engineering + 独有所需)' : '(全量上游)';
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
- const lines = [];
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 });
@@ -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
+ }
@@ -0,0 +1,5 @@
1
+ [
2
+ "grilling",
3
+ "grill-me",
4
+ "handoff"
5
+ ]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@heihei0299/matt-skills",
3
- "version": "3.0.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": {
@@ -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
- try {
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
- try {
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 formatTable(cmp) {
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(formatTable(res) + '\n');
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(formatTable(cmp) + '\n');
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
  // 清理临时目录