@heihei0299/matt-skills 3.0.30 → 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/bin/cli.js +15 -671
- 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/package.json +1 -1
- package/scripts/sync-upstream.js +2 -2
- /package/bin/{skill-config.js → skills/config.js} +0 -0
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { cp, mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { mergeManagedAgents, syncManagedAgents } from './agents.js';
|
|
5
|
+
import { pathExists, sameTree } from './filesystem.js';
|
|
6
|
+
|
|
7
|
+
const ROOT = path.join(path.dirname(fileURLToPath(import.meta.url)), '../..');
|
|
8
|
+
export const TEMPLATE_DIR = path.join(ROOT, 'template');
|
|
9
|
+
const DRY_RUN_PATHS = ['AGENTS.md', 'AGENTS.md.bak', '.opencode', '.pi', '.agents/skills', '.claude/skills'];
|
|
10
|
+
|
|
11
|
+
function shouldCopyTemplatePath(src) {
|
|
12
|
+
const relative = path.relative(TEMPLATE_DIR, src);
|
|
13
|
+
const parts = relative.split(path.sep);
|
|
14
|
+
return !(parts[0] === '.agents' && parts[1] === 'skills');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function copyTemplate(target) {
|
|
18
|
+
await cp(TEMPLATE_DIR, target, {
|
|
19
|
+
recursive: true,
|
|
20
|
+
force: true,
|
|
21
|
+
filter: shouldCopyTemplatePath,
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function copyDryRunInputs(target, stage) {
|
|
26
|
+
for (const relative of DRY_RUN_PATHS) {
|
|
27
|
+
const source = path.join(target, relative);
|
|
28
|
+
if (!await pathExists(source)) continue;
|
|
29
|
+
const destination = path.join(stage, relative);
|
|
30
|
+
await mkdir(path.dirname(destination), { recursive: true });
|
|
31
|
+
await cp(source, destination, { recursive: true, force: true });
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function compareDryRunTrees(target, stage) {
|
|
36
|
+
const result = { added: [], updated: [], removed: [] };
|
|
37
|
+
for (const relative of DRY_RUN_PATHS) {
|
|
38
|
+
const before = path.join(target, relative);
|
|
39
|
+
const after = path.join(stage, relative);
|
|
40
|
+
const beforeExists = await pathExists(before);
|
|
41
|
+
const afterExists = await pathExists(after);
|
|
42
|
+
if (!beforeExists && afterExists) result.added.push(relative);
|
|
43
|
+
else if (beforeExists && !afterExists) result.removed.push(relative);
|
|
44
|
+
else if (beforeExists && afterExists && !await sameTree(before, after)) result.updated.push(relative);
|
|
45
|
+
}
|
|
46
|
+
return result;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function formatTargetComparison({ target, result, onlyProgramming, refreshAgents }) {
|
|
50
|
+
const lines = [
|
|
51
|
+
`目标: ${target}`,
|
|
52
|
+
`范围: ${onlyProgramming ? '默认 workflow' : '全部可分发'}${refreshAgents ? ';刷新 AGENTS.md' : ''}`,
|
|
53
|
+
'',
|
|
54
|
+
];
|
|
55
|
+
const totalDiff = result.added.length + result.updated.length + result.removed.length;
|
|
56
|
+
if (totalDiff === 0) {
|
|
57
|
+
lines.push('✅ 无需更新');
|
|
58
|
+
return lines.join('\n');
|
|
59
|
+
}
|
|
60
|
+
if (result.added.length) lines.push(`新增 (${result.added.length}): ${result.added.join(', ')}`);
|
|
61
|
+
if (result.updated.length) lines.push(`更新 (${result.updated.length}): ${result.updated.join(', ')}`);
|
|
62
|
+
if (result.removed.length) lines.push(`删除 (${result.removed.length}): ${result.removed.join(', ')}`);
|
|
63
|
+
return lines.join('\n');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export async function refreshAgentsFile(targetFile) {
|
|
67
|
+
const templateFile = path.join(TEMPLATE_DIR, 'AGENTS.md');
|
|
68
|
+
const current = await readFile(targetFile, 'utf8');
|
|
69
|
+
const template = await readFile(templateFile, 'utf8');
|
|
70
|
+
const merged = mergeManagedAgents(current, template);
|
|
71
|
+
if (merged !== null) {
|
|
72
|
+
if (merged !== current) await writeFile(targetFile, merged);
|
|
73
|
+
return 'managed';
|
|
74
|
+
}
|
|
75
|
+
await cp(targetFile, `${targetFile}.bak`, { force: true });
|
|
76
|
+
await cp(templateFile, targetFile, { force: true });
|
|
77
|
+
return 'full';
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export async function syncTemplate({ target, refreshAgents }) {
|
|
81
|
+
const marker = path.join(target, 'AGENTS.md');
|
|
82
|
+
if (!(await pathExists(marker))) {
|
|
83
|
+
await copyTemplate(target);
|
|
84
|
+
return { initialized: true, agentsManaged: false, agentsRefreshed: false, agentsRefreshMode: null };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
let agentsManaged = false;
|
|
88
|
+
let agentsRefreshed = false;
|
|
89
|
+
let agentsRefreshMode = null;
|
|
90
|
+
if (refreshAgents) {
|
|
91
|
+
agentsRefreshMode = await refreshAgentsFile(marker);
|
|
92
|
+
agentsRefreshed = true;
|
|
93
|
+
} else {
|
|
94
|
+
try {
|
|
95
|
+
agentsManaged = await syncManagedAgents(marker, path.join(TEMPLATE_DIR, 'AGENTS.md'));
|
|
96
|
+
} catch {}
|
|
97
|
+
}
|
|
98
|
+
await cp(path.join(TEMPLATE_DIR, '.opencode'), path.join(target, '.opencode'), { recursive: true, force: true });
|
|
99
|
+
await cp(path.join(TEMPLATE_DIR, '.pi'), path.join(target, '.pi'), { recursive: true, force: true });
|
|
100
|
+
return { initialized: false, agentsManaged, agentsRefreshed, agentsRefreshMode };
|
|
101
|
+
}
|
|
@@ -2,7 +2,7 @@ import { readFileSync } from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
4
|
|
|
5
|
-
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '
|
|
5
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');
|
|
6
6
|
const CONFIG_PATH = path.join(ROOT, 'config', 'proprietary.json');
|
|
7
7
|
const config = JSON.parse(readFileSync(CONFIG_PATH, 'utf8'));
|
|
8
8
|
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { readdir, readFile } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { resolveSkillNames } from './selection.js';
|
|
5
|
+
import { loadSkillSet } from './config.js';
|
|
6
|
+
|
|
7
|
+
const ROOT = path.join(path.dirname(fileURLToPath(import.meta.url)), '../..');
|
|
8
|
+
export const SKILLS_DIR = path.join(ROOT, '.agents', 'skills');
|
|
9
|
+
const DEFAULT_PATH = path.join(ROOT, 'config', 'default.json');
|
|
10
|
+
let DEFAULT_SKILLS = null;
|
|
11
|
+
|
|
12
|
+
async function loadDefaultSkills() {
|
|
13
|
+
if (!DEFAULT_SKILLS) DEFAULT_SKILLS = await loadSkillSet(DEFAULT_PATH, 'default');
|
|
14
|
+
return DEFAULT_SKILLS;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function parseFrontmatter(text) {
|
|
18
|
+
const match = text.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
19
|
+
if (!match) return {};
|
|
20
|
+
const fields = {};
|
|
21
|
+
for (const line of match[1].split(/\r?\n/)) {
|
|
22
|
+
const m = line.match(/^([A-Za-z0-9_-]+):\s*(.*)$/);
|
|
23
|
+
if (!m) continue;
|
|
24
|
+
let value = m[2].trim();
|
|
25
|
+
if (value.startsWith('"') && value.endsWith('"')) {
|
|
26
|
+
value = value.slice(1, -1).replace(/\\"/g, '"').replace(/\\\\/g, '\\');
|
|
27
|
+
}
|
|
28
|
+
fields[m[1]] = value;
|
|
29
|
+
}
|
|
30
|
+
return fields;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function listAvailableSkillNames() {
|
|
34
|
+
const entries = await readdir(SKILLS_DIR, { withFileTypes: true });
|
|
35
|
+
return entries
|
|
36
|
+
.filter((entry) => entry.isDirectory())
|
|
37
|
+
.filter((entry) => !entry.name.endsWith('.bak'))
|
|
38
|
+
.filter((entry) => entry.name !== 'skill-creator' && entry.name !== '.git')
|
|
39
|
+
.map((entry) => entry.name);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function listSkillNames({ onlyProgramming = false } = {}) {
|
|
43
|
+
return resolveSkillNames({
|
|
44
|
+
availableNames: await listAvailableSkillNames(),
|
|
45
|
+
mode: onlyProgramming ? 'default' : 'all',
|
|
46
|
+
defaults: onlyProgramming ? await loadDefaultSkills() : [],
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export async function listSkills({ onlyProgramming = false } = {}) {
|
|
51
|
+
const skills = [];
|
|
52
|
+
for (const name of await listSkillNames({ onlyProgramming })) {
|
|
53
|
+
let content;
|
|
54
|
+
try {
|
|
55
|
+
content = await readFile(path.join(SKILLS_DIR, name, 'SKILL.md'), 'utf8');
|
|
56
|
+
} catch {
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
const { description } = parseFrontmatter(content);
|
|
60
|
+
if (description) skills.push({ name, description });
|
|
61
|
+
}
|
|
62
|
+
return skills.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
|
|
63
|
+
}
|
package/package.json
CHANGED
package/scripts/sync-upstream.js
CHANGED
|
@@ -5,8 +5,8 @@ import { spawnSync } from 'node:child_process';
|
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
import os from 'node:os';
|
|
7
7
|
import { fileURLToPath } from 'node:url';
|
|
8
|
-
import { PROPRIETARY_SKILLS } from '../bin/
|
|
9
|
-
import { loadSkillSet } from '../bin/
|
|
8
|
+
import { PROPRIETARY_SKILLS } from '../bin/skills/boundaries.js';
|
|
9
|
+
import { loadSkillSet } from '../bin/skills/config.js';
|
|
10
10
|
|
|
11
11
|
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
12
12
|
const LOCAL_SKILLS_DIR = path.join(ROOT, '.agents', 'skills');
|
|
File without changes
|