agentic-awesome-skills 14.0.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/LICENSE +21 -0
- package/README.md +577 -0
- package/package.json +93 -0
- package/tools/bin/install.js +755 -0
- package/tools/lib/project-root.js +25 -0
- package/tools/lib/skill-filter.js +110 -0
- package/tools/lib/skill-utils.js +226 -0
- package/tools/lib/symlink-safety.js +27 -0
- package/tools/lib/workflow-contract.js +197 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
|
|
4
|
+
function findProjectRoot(startDir = __dirname) {
|
|
5
|
+
let current = path.resolve(startDir);
|
|
6
|
+
|
|
7
|
+
while (true) {
|
|
8
|
+
if (
|
|
9
|
+
fs.existsSync(path.join(current, "package.json")) &&
|
|
10
|
+
fs.existsSync(path.join(current, "README.md"))
|
|
11
|
+
) {
|
|
12
|
+
return current;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const parent = path.dirname(current);
|
|
16
|
+
if (parent === current) {
|
|
17
|
+
throw new Error(`Could not locate project root from ${startDir}`);
|
|
18
|
+
}
|
|
19
|
+
current = parent;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
module.exports = {
|
|
24
|
+
findProjectRoot,
|
|
25
|
+
};
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skill Filtering Utility - Fixes Issue #215 (Gemini Token Truncation)
|
|
3
|
+
*
|
|
4
|
+
* This module provides utilities to filter and manage skills by category,
|
|
5
|
+
* reducing context overhead when all skills exceed token limits.
|
|
6
|
+
*
|
|
7
|
+
* Issue: When all 1,200+ skills are loaded, they collectively exceed Gemini's
|
|
8
|
+
* token limit for chat message conversion, causing truncation errors.
|
|
9
|
+
*
|
|
10
|
+
* Solution: Filter skills by category to reduce context size
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const fs = require('fs');
|
|
14
|
+
const path = require('path');
|
|
15
|
+
|
|
16
|
+
// Skill categories and their typical token weights (estimated)
|
|
17
|
+
const SKILL_CATEGORIES = {
|
|
18
|
+
'core': { description: 'Essential/core skills', weight: 0.5 },
|
|
19
|
+
'ai-agents': { description: 'AI agents and multi-agent patterns', weight: 1.0 },
|
|
20
|
+
'architecture': { description: 'System architecture and design', weight: 0.8 },
|
|
21
|
+
'frontend': { description: 'Frontend development', weight: 0.7 },
|
|
22
|
+
'backend': { description: 'Backend development', weight: 0.7 },
|
|
23
|
+
'devops': { description: 'DevOps and infrastructure', weight: 0.9 },
|
|
24
|
+
'security': { description: 'Security and auditing', weight: 0.8 },
|
|
25
|
+
'testing': { description: 'Testing strategies', weight: 0.6 },
|
|
26
|
+
'documentation': { description: 'Documentation and technical writing', weight: 0.5 },
|
|
27
|
+
'performance': { description: 'Performance optimization', weight: 0.6 }
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// Recommended bundles for different token limits
|
|
31
|
+
const SKILL_BUNDLES = {
|
|
32
|
+
'minimal': {
|
|
33
|
+
description: 'Minimal bundle for Gemini (solves issue #215)',
|
|
34
|
+
categories: ['development', 'devops'],
|
|
35
|
+
estimatedTokens: 5000,
|
|
36
|
+
note: 'Recommended for environments with strict token limits'
|
|
37
|
+
},
|
|
38
|
+
'balanced': {
|
|
39
|
+
description: 'Balanced bundle with essential and development skills',
|
|
40
|
+
categories: ['development', 'devops', 'security', 'documentation'],
|
|
41
|
+
estimatedTokens: 15000,
|
|
42
|
+
note: 'Good for most development scenarios'
|
|
43
|
+
},
|
|
44
|
+
'complete': {
|
|
45
|
+
description: 'Complete bundle with all skill categories',
|
|
46
|
+
categories: Object.keys(SKILL_CATEGORIES),
|
|
47
|
+
estimatedTokens: 50000,
|
|
48
|
+
note: 'Full suite - may exceed token limits on some models'
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Filter skills by category
|
|
54
|
+
* @param {Array} skills - Array of skill objects
|
|
55
|
+
* @param {Array} categories - Array of category names to include
|
|
56
|
+
* @returns {Array} Filtered skills
|
|
57
|
+
*/
|
|
58
|
+
function filterSkillsByCategory(skills, categories = ['core']) {
|
|
59
|
+
if (!Array.isArray(skills)) return [];
|
|
60
|
+
if (!Array.isArray(categories) || categories.length === 0) {
|
|
61
|
+
categories = ['core'];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return skills.filter(skill => {
|
|
65
|
+
const skillCategory = skill.category || skill.tags?.[0];
|
|
66
|
+
return categories.includes(skillCategory);
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Get skills by bundle name
|
|
72
|
+
* @param {Array} skills - Array of skill objects
|
|
73
|
+
* @param {String} bundleName - Bundle name (minimal, balanced, complete)
|
|
74
|
+
* @returns {Array} Skills matching the bundle
|
|
75
|
+
*/
|
|
76
|
+
function getSkillsByBundle(skills, bundleName = 'minimal') {
|
|
77
|
+
if (!Array.isArray(skills)) return [];
|
|
78
|
+
|
|
79
|
+
if (bundleName === 'complete') {
|
|
80
|
+
return [...skills];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const bundle = SKILL_BUNDLES[bundleName];
|
|
84
|
+
if (!bundle) {
|
|
85
|
+
console.warn(`Unknown bundle: ${bundleName}. Using 'minimal' bundle.`);
|
|
86
|
+
return filterSkillsByCategory(skills, SKILL_BUNDLES['minimal'].categories);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return filterSkillsByCategory(skills, bundle.categories);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Get bundle info and recommendations
|
|
94
|
+
* @returns {Object} Bundle information
|
|
95
|
+
*/
|
|
96
|
+
function getBundleInfo() {
|
|
97
|
+
return {
|
|
98
|
+
bundles: SKILL_BUNDLES,
|
|
99
|
+
categories: SKILL_CATEGORIES,
|
|
100
|
+
recommendation: 'Use "minimal" or "balanced" bundle for Gemini to avoid token truncation errors (#215)'
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
module.exports = {
|
|
105
|
+
SKILL_CATEGORIES,
|
|
106
|
+
SKILL_BUNDLES,
|
|
107
|
+
filterSkillsByCategory,
|
|
108
|
+
getSkillsByBundle,
|
|
109
|
+
getBundleInfo
|
|
110
|
+
};
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const yaml = require('yaml');
|
|
4
|
+
|
|
5
|
+
function isSafeDirectory(dirPath) {
|
|
6
|
+
try {
|
|
7
|
+
const stats = fs.lstatSync(dirPath);
|
|
8
|
+
return stats.isDirectory() && !stats.isSymbolicLink();
|
|
9
|
+
} catch {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function isSafeSkillFile(skillPath) {
|
|
15
|
+
try {
|
|
16
|
+
const stats = fs.lstatSync(skillPath);
|
|
17
|
+
return stats.isFile() && !stats.isSymbolicLink();
|
|
18
|
+
} catch {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function stripQuotes(value) {
|
|
24
|
+
if (typeof value !== 'string') return value;
|
|
25
|
+
if (value.length < 2) return value.trim();
|
|
26
|
+
const first = value[0];
|
|
27
|
+
const last = value[value.length - 1];
|
|
28
|
+
if ((first === '"' && last === '"') || (first === "'" && last === "'")) {
|
|
29
|
+
return value.slice(1, -1).trim();
|
|
30
|
+
}
|
|
31
|
+
if (first === '"' || first === "'") {
|
|
32
|
+
return value.slice(1).trim();
|
|
33
|
+
}
|
|
34
|
+
if (last === '"' || last === "'") {
|
|
35
|
+
return value.slice(0, -1).trim();
|
|
36
|
+
}
|
|
37
|
+
return value.trim();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function parseInlineList(raw) {
|
|
41
|
+
if (typeof raw !== 'string') return [];
|
|
42
|
+
const value = raw.trim();
|
|
43
|
+
if (!value.startsWith('[') || !value.endsWith(']')) return [];
|
|
44
|
+
const inner = value.slice(1, -1).trim();
|
|
45
|
+
if (!inner) return [];
|
|
46
|
+
return inner
|
|
47
|
+
.split(',')
|
|
48
|
+
.map(item => stripQuotes(item.trim()))
|
|
49
|
+
.filter(Boolean);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function isPlainObject(value) {
|
|
53
|
+
return value && typeof value === 'object' && !Array.isArray(value);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function parseFrontmatter(content) {
|
|
57
|
+
const sanitized = content.replace(/^\uFEFF/, '');
|
|
58
|
+
const lines = sanitized.split(/\r?\n/);
|
|
59
|
+
if (!lines.length || lines[0].trim() !== '---') {
|
|
60
|
+
return { data: {}, body: content, errors: [], hasFrontmatter: false };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
let endIndex = -1;
|
|
64
|
+
for (let i = 1; i < lines.length; i += 1) {
|
|
65
|
+
if (lines[i].trim() === '---') {
|
|
66
|
+
endIndex = i;
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (endIndex === -1) {
|
|
72
|
+
return {
|
|
73
|
+
data: {},
|
|
74
|
+
body: content,
|
|
75
|
+
errors: ['Missing closing frontmatter delimiter (---).'],
|
|
76
|
+
hasFrontmatter: true,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const errors = [];
|
|
81
|
+
const fmText = lines.slice(1, endIndex).join('\n');
|
|
82
|
+
let data = {};
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
const doc = yaml.parseDocument(fmText, { prettyErrors: false });
|
|
86
|
+
if (doc.errors && doc.errors.length) {
|
|
87
|
+
errors.push(...doc.errors.map(error => error.message));
|
|
88
|
+
}
|
|
89
|
+
data = doc.toJS();
|
|
90
|
+
} catch (err) {
|
|
91
|
+
errors.push(err.message);
|
|
92
|
+
data = {};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (!isPlainObject(data)) {
|
|
96
|
+
errors.push('Frontmatter must be a YAML mapping/object.');
|
|
97
|
+
data = {};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const body = lines.slice(endIndex + 1).join('\n');
|
|
101
|
+
return { data, body, errors, hasFrontmatter: true };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function tokenize(value) {
|
|
105
|
+
if (!value) return [];
|
|
106
|
+
return value
|
|
107
|
+
.toLowerCase()
|
|
108
|
+
.replace(/[^a-z0-9]+/g, ' ')
|
|
109
|
+
.split(' ')
|
|
110
|
+
.map(token => token.trim())
|
|
111
|
+
.filter(Boolean);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function unique(list) {
|
|
115
|
+
const seen = new Set();
|
|
116
|
+
const result = [];
|
|
117
|
+
for (const item of list) {
|
|
118
|
+
if (!item || seen.has(item)) continue;
|
|
119
|
+
seen.add(item);
|
|
120
|
+
result.push(item);
|
|
121
|
+
}
|
|
122
|
+
return result;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function readSkill(skillDir, skillId) {
|
|
126
|
+
const skillPath = path.join(skillDir, skillId, 'SKILL.md');
|
|
127
|
+
const content = fs.readFileSync(skillPath, 'utf8');
|
|
128
|
+
const { data } = parseFrontmatter(content);
|
|
129
|
+
const name = typeof data.name === 'string' && data.name.trim()
|
|
130
|
+
? data.name.trim()
|
|
131
|
+
: skillId;
|
|
132
|
+
const description = typeof data.description === 'string'
|
|
133
|
+
? data.description.trim()
|
|
134
|
+
: '';
|
|
135
|
+
|
|
136
|
+
let tags = [];
|
|
137
|
+
if (Array.isArray(data.tags)) {
|
|
138
|
+
tags = data.tags.map(tag => String(tag).trim());
|
|
139
|
+
} else if (typeof data.tags === 'string' && data.tags.trim()) {
|
|
140
|
+
const inlineTags = parseInlineList(data.tags);
|
|
141
|
+
const parts = inlineTags.length > 0
|
|
142
|
+
? inlineTags
|
|
143
|
+
: (data.tags.includes(',') ? data.tags.split(',') : data.tags.split(/\s+/));
|
|
144
|
+
tags = parts.map(tag => tag.trim());
|
|
145
|
+
} else if (isPlainObject(data.metadata) && data.metadata.tags) {
|
|
146
|
+
const rawTags = data.metadata.tags;
|
|
147
|
+
if (Array.isArray(rawTags)) {
|
|
148
|
+
tags = rawTags.map(tag => String(tag).trim());
|
|
149
|
+
} else if (typeof rawTags === 'string' && rawTags.trim()) {
|
|
150
|
+
const inlineTags = parseInlineList(rawTags);
|
|
151
|
+
const parts = inlineTags.length > 0
|
|
152
|
+
? inlineTags
|
|
153
|
+
: (rawTags.includes(',') ? rawTags.split(',') : rawTags.split(/\s+/));
|
|
154
|
+
tags = parts.map(tag => tag.trim());
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
tags = tags.filter(Boolean);
|
|
159
|
+
const category = typeof data.category === 'string' ? data.category.trim() : '';
|
|
160
|
+
const risk = typeof data.risk === 'string' ? data.risk.trim() : '';
|
|
161
|
+
|
|
162
|
+
return {
|
|
163
|
+
id: skillId,
|
|
164
|
+
name,
|
|
165
|
+
description,
|
|
166
|
+
category,
|
|
167
|
+
risk,
|
|
168
|
+
tags,
|
|
169
|
+
path: skillPath,
|
|
170
|
+
content,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function listSkillIds(skillsDir) {
|
|
175
|
+
return fs.readdirSync(skillsDir)
|
|
176
|
+
.filter(entry => {
|
|
177
|
+
if (entry.startsWith('.')) return false;
|
|
178
|
+
const dirPath = path.join(skillsDir, entry);
|
|
179
|
+
if (!isSafeDirectory(dirPath)) return false;
|
|
180
|
+
const skillPath = path.join(dirPath, 'SKILL.md');
|
|
181
|
+
return isSafeSkillFile(skillPath);
|
|
182
|
+
})
|
|
183
|
+
.sort();
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Recursively list all skill directory paths under skillsDir (relative paths).
|
|
188
|
+
* Matches generate_index.py behavior so catalog includes nested skills (e.g. game-development/2d-games).
|
|
189
|
+
*/
|
|
190
|
+
function listSkillIdsRecursive(skillsDir, baseDir = skillsDir, acc = []) {
|
|
191
|
+
const stack = [baseDir];
|
|
192
|
+
const visited = new Set();
|
|
193
|
+
|
|
194
|
+
while (stack.length > 0) {
|
|
195
|
+
const currentDir = stack.pop();
|
|
196
|
+
const resolvedDir = path.resolve(currentDir);
|
|
197
|
+
if (visited.has(resolvedDir)) continue;
|
|
198
|
+
visited.add(resolvedDir);
|
|
199
|
+
|
|
200
|
+
const entries = fs.readdirSync(currentDir, { withFileTypes: true });
|
|
201
|
+
for (const entry of entries) {
|
|
202
|
+
if (entry.name.startsWith('.')) continue;
|
|
203
|
+
const dirPath = path.join(currentDir, entry.name);
|
|
204
|
+
if (!isSafeDirectory(dirPath)) continue;
|
|
205
|
+
|
|
206
|
+
const skillPath = path.join(dirPath, 'SKILL.md');
|
|
207
|
+
const relPath = path.relative(skillsDir, dirPath).split(path.sep).join('/');
|
|
208
|
+
if (isSafeSkillFile(skillPath)) {
|
|
209
|
+
acc.push(relPath);
|
|
210
|
+
}
|
|
211
|
+
stack.push(dirPath);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return acc.sort();
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
module.exports = {
|
|
218
|
+
listSkillIds,
|
|
219
|
+
listSkillIdsRecursive,
|
|
220
|
+
parseFrontmatter,
|
|
221
|
+
parseInlineList,
|
|
222
|
+
readSkill,
|
|
223
|
+
stripQuotes,
|
|
224
|
+
tokenize,
|
|
225
|
+
unique,
|
|
226
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
|
|
4
|
+
function isPathInside(basePath, candidatePath) {
|
|
5
|
+
const base = fs.existsSync(basePath) ? getRealPath(basePath) : path.resolve(basePath);
|
|
6
|
+
const candidate = path.resolve(candidatePath);
|
|
7
|
+
const relative = path.relative(base, candidate);
|
|
8
|
+
return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function getRealPath(targetPath) {
|
|
12
|
+
if (typeof fs.realpathSync.native === "function") {
|
|
13
|
+
return fs.realpathSync.native(targetPath);
|
|
14
|
+
}
|
|
15
|
+
return fs.realpathSync(targetPath);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function resolveSafeRealPath(rootPath, targetPath) {
|
|
19
|
+
const realPath = getRealPath(targetPath);
|
|
20
|
+
return isPathInside(rootPath, realPath) ? realPath : null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
module.exports = {
|
|
24
|
+
getRealPath,
|
|
25
|
+
isPathInside,
|
|
26
|
+
resolveSafeRealPath,
|
|
27
|
+
};
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
|
|
4
|
+
const { findProjectRoot } = require("./project-root");
|
|
5
|
+
|
|
6
|
+
const DOC_PREFIXES = ["docs/"];
|
|
7
|
+
const DOC_FILES = new Set(["README.md", "CONTRIBUTING.md", "CHANGELOG.md", "walkthrough.md"]);
|
|
8
|
+
const INFRA_PREFIXES = [".github/", "tools/", "apps/"];
|
|
9
|
+
const INFRA_FILES = new Set(["package.json", "package-lock.json"]);
|
|
10
|
+
const REFERENCES_PREFIXES = ["docs/", ".github/", "tools/", "apps/", "data/"];
|
|
11
|
+
const REFERENCES_FILES = new Set([
|
|
12
|
+
"README.md",
|
|
13
|
+
"CONTRIBUTING.md",
|
|
14
|
+
"CHANGELOG.md",
|
|
15
|
+
"walkthrough.md",
|
|
16
|
+
"package.json",
|
|
17
|
+
"package-lock.json",
|
|
18
|
+
]);
|
|
19
|
+
|
|
20
|
+
function normalizeRepoPath(filePath) {
|
|
21
|
+
return String(filePath || "").replace(/\\/g, "/").replace(/^\.\//, "");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function matchesContractEntry(filePath, entry) {
|
|
25
|
+
const normalizedPath = normalizeRepoPath(filePath);
|
|
26
|
+
const normalizedEntry = normalizeRepoPath(entry);
|
|
27
|
+
|
|
28
|
+
if (!normalizedEntry) {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (normalizedEntry.endsWith("/")) {
|
|
33
|
+
return normalizedPath.startsWith(normalizedEntry);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return normalizedPath === normalizedEntry;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function escapeRegExp(value) {
|
|
40
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function loadWorkflowContract(startDir = __dirname) {
|
|
44
|
+
const projectRoot = findProjectRoot(startDir);
|
|
45
|
+
const configPath = path.join(projectRoot, "tools", "config", "generated-files.json");
|
|
46
|
+
const rawConfig = fs.readFileSync(configPath, "utf8");
|
|
47
|
+
const config = JSON.parse(rawConfig);
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
projectRoot,
|
|
51
|
+
configPath,
|
|
52
|
+
derivedFiles: config.derivedFiles.map(normalizeRepoPath),
|
|
53
|
+
mixedFiles: config.mixedFiles.map(normalizeRepoPath),
|
|
54
|
+
releaseManagedFiles: config.releaseManagedFiles.map(normalizeRepoPath),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function getManagedFiles(contract, options = {}) {
|
|
59
|
+
const includeMixed = Boolean(options.includeMixed);
|
|
60
|
+
const includeReleaseManaged = Boolean(options.includeReleaseManaged);
|
|
61
|
+
const managedFiles = [...contract.derivedFiles];
|
|
62
|
+
|
|
63
|
+
if (includeMixed) {
|
|
64
|
+
managedFiles.push(...contract.mixedFiles);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (includeReleaseManaged) {
|
|
68
|
+
managedFiles.push(...contract.releaseManagedFiles);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return [...new Set(managedFiles.map(normalizeRepoPath))];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function isDerivedFile(filePath, contract) {
|
|
75
|
+
return contract.derivedFiles.some((entry) => matchesContractEntry(filePath, entry));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function isMixedFile(filePath, contract) {
|
|
79
|
+
return contract.mixedFiles.some((entry) => matchesContractEntry(filePath, entry));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function isDocLikeFile(filePath) {
|
|
83
|
+
const normalized = normalizeRepoPath(filePath);
|
|
84
|
+
return normalized.endsWith(".md") || DOC_FILES.has(normalized) || DOC_PREFIXES.some((prefix) => normalized.startsWith(prefix));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function isInfraLikeFile(filePath) {
|
|
88
|
+
const normalized = normalizeRepoPath(filePath);
|
|
89
|
+
return (
|
|
90
|
+
INFRA_FILES.has(normalized) ||
|
|
91
|
+
INFRA_PREFIXES.some((prefix) => normalized.startsWith(prefix))
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function classifyChangedFiles(changedFiles, contract) {
|
|
96
|
+
const categories = new Set();
|
|
97
|
+
const normalizedFiles = changedFiles.map(normalizeRepoPath).filter(Boolean);
|
|
98
|
+
|
|
99
|
+
for (const filePath of normalizedFiles) {
|
|
100
|
+
if (isDerivedFile(filePath, contract)) {
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const isSkillPath = filePath.startsWith("skills/");
|
|
105
|
+
|
|
106
|
+
if (isSkillPath) {
|
|
107
|
+
categories.add("skill");
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (!isSkillPath && (isDocLikeFile(filePath) || isMixedFile(filePath, contract))) {
|
|
111
|
+
categories.add("docs");
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (isInfraLikeFile(filePath)) {
|
|
115
|
+
categories.add("infra");
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const orderedCategories = ["skill", "docs", "infra"].filter((category) => categories.has(category));
|
|
120
|
+
let primaryCategory = "none";
|
|
121
|
+
if (orderedCategories.includes("infra")) {
|
|
122
|
+
primaryCategory = "infra";
|
|
123
|
+
} else if (orderedCategories.includes("skill")) {
|
|
124
|
+
primaryCategory = "skill";
|
|
125
|
+
} else if (orderedCategories.includes("docs")) {
|
|
126
|
+
primaryCategory = "docs";
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
categories: orderedCategories,
|
|
131
|
+
primaryCategory,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function getDirectDerivedChanges(changedFiles, contract) {
|
|
136
|
+
return changedFiles
|
|
137
|
+
.map(normalizeRepoPath)
|
|
138
|
+
.filter(Boolean)
|
|
139
|
+
.filter((filePath) => isDerivedFile(filePath, contract));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function requiresReferencesValidation(changedFiles, contract) {
|
|
143
|
+
return changedFiles
|
|
144
|
+
.map(normalizeRepoPath)
|
|
145
|
+
.filter(Boolean)
|
|
146
|
+
.some((filePath) => {
|
|
147
|
+
if (isDerivedFile(filePath, contract) || isMixedFile(filePath, contract)) {
|
|
148
|
+
return true;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return (
|
|
152
|
+
REFERENCES_FILES.has(filePath) ||
|
|
153
|
+
REFERENCES_PREFIXES.some((prefix) => filePath.startsWith(prefix))
|
|
154
|
+
);
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function extractChangelogSection(content, version) {
|
|
159
|
+
const headingExpression = new RegExp(`^## \\[${escapeRegExp(version)}\\].*$`, "m");
|
|
160
|
+
const headingMatch = headingExpression.exec(content);
|
|
161
|
+
if (!headingMatch) {
|
|
162
|
+
throw new Error(`CHANGELOG.md does not contain a section for version ${version}.`);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const startIndex = headingMatch.index;
|
|
166
|
+
const remainder = content.slice(startIndex + headingMatch[0].length);
|
|
167
|
+
const nextSectionRelativeIndex = remainder.search(/^## \[/m);
|
|
168
|
+
const endIndex =
|
|
169
|
+
nextSectionRelativeIndex === -1
|
|
170
|
+
? content.length
|
|
171
|
+
: startIndex + headingMatch[0].length + nextSectionRelativeIndex;
|
|
172
|
+
|
|
173
|
+
return `${content.slice(startIndex, endIndex).trim()}\n`;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function hasQualityChecklist(body) {
|
|
177
|
+
return /quality bar checklist/i.test(String(body || ""));
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function hasIssueLink(body) {
|
|
181
|
+
return /(?:closes|fixes)\s+#\d+/i.test(String(body || ""));
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
module.exports = {
|
|
185
|
+
classifyChangedFiles,
|
|
186
|
+
extractChangelogSection,
|
|
187
|
+
getDirectDerivedChanges,
|
|
188
|
+
getManagedFiles,
|
|
189
|
+
hasIssueLink,
|
|
190
|
+
hasQualityChecklist,
|
|
191
|
+
isDerivedFile,
|
|
192
|
+
isMixedFile,
|
|
193
|
+
loadWorkflowContract,
|
|
194
|
+
normalizeRepoPath,
|
|
195
|
+
matchesContractEntry,
|
|
196
|
+
requiresReferencesValidation,
|
|
197
|
+
};
|