Add metadata.json and its extraction script
This commit is contained in:
parent
db0d47413a
commit
9b8b80b0ec
2
.github/copilot-instructions.md
vendored
2
.github/copilot-instructions.md
vendored
@ -1,5 +1,7 @@
|
|||||||
When performing a code review, validate that there are changes in the `README.md` file that match the changes in the pull request. If there are no changes, or if the changes do not match, then the pull request is not ready to be merged.
|
When performing a code review, validate that there are changes in the `README.md` file that match the changes in the pull request. If there are no changes, or if the changes do not match, then the pull request is not ready to be merged.
|
||||||
|
|
||||||
|
When performing a code review, validate that there are changes in the `metadata.json` file that match the changes in the pull request. If there are no changes, or if the changes do not match, then the pull request is not ready to be merged.
|
||||||
|
|
||||||
When performing a code review, ensure that the values in the front matter are wrapped in single quotes.
|
When performing a code review, ensure that the values in the front matter are wrapped in single quotes.
|
||||||
|
|
||||||
When performing a code review, ensure that the `description` field in the front matter is not empty.
|
When performing a code review, ensure that the `description` field in the front matter is not empty.
|
||||||
|
|||||||
79
.github/workflows/validate-metadata.yml
vendored
Normal file
79
.github/workflows/validate-metadata.yml
vendored
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
name: Validate metadata.json
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
types: [opened, synchronize, reopened]
|
||||||
|
paths:
|
||||||
|
- "instructions/**"
|
||||||
|
- "prompts/**"
|
||||||
|
- "chatmodes/**"
|
||||||
|
- "*.js"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
validate-metadata:
|
||||||
|
permissions:
|
||||||
|
pull-requests: write
|
||||||
|
contents: read
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: "20"
|
||||||
|
|
||||||
|
- name: Update metadata.json
|
||||||
|
run: node update-metadata.js
|
||||||
|
|
||||||
|
- name: Check for metadata.json changes
|
||||||
|
id: check-diff
|
||||||
|
run: |
|
||||||
|
if git diff --exit-code metadata.json; then
|
||||||
|
echo "No changes to metadata.json after running update script."
|
||||||
|
echo "status=success" >> $GITHUB_OUTPUT
|
||||||
|
else
|
||||||
|
echo "Changes detected in metadata.json after running update script."
|
||||||
|
echo "status=failure" >> $GITHUB_OUTPUT
|
||||||
|
echo "diff<<EOF" >> $GITHUB_OUTPUT
|
||||||
|
git diff metadata.json >> $GITHUB_OUTPUT
|
||||||
|
echo "EOF" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Output diff to logs for non-write users
|
||||||
|
if: steps.check-diff.outputs.status == 'failure' && github.event.pull_request.head.repo.permissions.push != true
|
||||||
|
run: |
|
||||||
|
echo "::group::metadata.json diff (changes needed)"
|
||||||
|
echo "The following changes need to be made to metadata.json:"
|
||||||
|
echo ""
|
||||||
|
git diff metadata.json
|
||||||
|
echo "::endgroup::"
|
||||||
|
|
||||||
|
- name: Comment on PR if metadata.json needs updating
|
||||||
|
if: steps.check-diff.outputs.status == 'failure' && github.event.pull_request.head.repo.permissions.push == true
|
||||||
|
uses: marocchino/sticky-pull-request-comment@v2
|
||||||
|
with:
|
||||||
|
header: metadata-validation
|
||||||
|
message: |
|
||||||
|
## ⚠️ `metadata.json` needs to be updated
|
||||||
|
|
||||||
|
The `update-metadata.js` script detected changes that need to be made to the `metadata.json` file.
|
||||||
|
|
||||||
|
Please run `node update-metadata.js` locally and commit the changes before merging this PR.
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>View diff</summary>
|
||||||
|
|
||||||
|
```diff
|
||||||
|
${{ steps.check-diff.outputs.diff }}
|
||||||
|
```
|
||||||
|
</details>
|
||||||
|
|
||||||
|
- name: Fail workflow if metadata.json needs updating
|
||||||
|
if: steps.check-diff.outputs.status == 'failure'
|
||||||
|
run: |
|
||||||
|
echo "❌ `metadata.json` needs to be updated. Please run 'node update-metadata.js' locally and commit the changes."
|
||||||
|
exit 1
|
||||||
121
frontmatter-schema.json
Normal file
121
frontmatter-schema.json
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
{
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
|
"title": "Frontmatter Schema",
|
||||||
|
"description": "Schema for validating frontmatter data in the awesome-copilot repository",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"chatmodes": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "Array of chat mode configurations",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/chatmode"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"instructions": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "Array of instruction file configurations",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/instruction"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"prompts": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "Array of prompt file configurations",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/prompt"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false,
|
||||||
|
"definitions": {
|
||||||
|
"chatmode": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "Configuration for a chat mode",
|
||||||
|
"properties": {
|
||||||
|
"filename": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Name of the chat mode file",
|
||||||
|
"pattern": "^[a-zA-Z0-9._-]+\\.chatmode\\.md$"
|
||||||
|
},
|
||||||
|
"title": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Display title for the chat mode",
|
||||||
|
"minLength": 1
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Description of the chat mode functionality",
|
||||||
|
"minLength": 1
|
||||||
|
},
|
||||||
|
"model": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "AI model to use for this chat mode"
|
||||||
|
},
|
||||||
|
"tools": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "Array of available tools for this chat mode",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["filename", "description"],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"instruction": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "Configuration for an instruction file",
|
||||||
|
"properties": {
|
||||||
|
"filename": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Name of the instruction file",
|
||||||
|
"pattern": "^[a-zA-Z0-9._-]+\\.instructions\\.md$"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Description of the instruction file purpose",
|
||||||
|
"minLength": 1
|
||||||
|
},
|
||||||
|
"applyTo": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "File patterns that this instruction applies to",
|
||||||
|
"items": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["filename", "description"],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"prompt": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "Configuration for a prompt file",
|
||||||
|
"properties": {
|
||||||
|
"filename": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Name of the prompt file",
|
||||||
|
"pattern": "^[a-zA-Z0-9._-]+\\.prompt\\.md$"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Description of the prompt functionality",
|
||||||
|
"minLength": 1
|
||||||
|
},
|
||||||
|
"mode": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Execution mode for the prompt"
|
||||||
|
},
|
||||||
|
"tools": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "Array of available tools for this prompt",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["filename", "description"],
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
1647
metadata.json
Normal file
1647
metadata.json
Normal file
File diff suppressed because it is too large
Load Diff
241
update-metadata.js
Normal file
241
update-metadata.js
Normal file
@ -0,0 +1,241 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
// Read the JSON schema to understand the structure
|
||||||
|
const schemaPath = path.join(__dirname, 'frontmatter-schema.json');
|
||||||
|
const schema = JSON.parse(fs.readFileSync(schemaPath, 'utf8'));
|
||||||
|
|
||||||
|
// Define the directories to process
|
||||||
|
const directories = {
|
||||||
|
chatmodes: path.join(__dirname, 'chatmodes'),
|
||||||
|
instructions: path.join(__dirname, 'instructions'),
|
||||||
|
prompts: path.join(__dirname, 'prompts')
|
||||||
|
};
|
||||||
|
|
||||||
|
// Simple YAML parser for frontmatter
|
||||||
|
function parseSimpleYaml(yamlContent) {
|
||||||
|
const result = {};
|
||||||
|
const lines = yamlContent.split('\n');
|
||||||
|
let currentKey = null;
|
||||||
|
let currentValue = '';
|
||||||
|
let inArray = false;
|
||||||
|
let arrayItems = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < lines.length; i++) {
|
||||||
|
const line = lines[i];
|
||||||
|
const trimmed = line.trim();
|
||||||
|
|
||||||
|
if (!trimmed || trimmed.startsWith('#')) continue;
|
||||||
|
|
||||||
|
// Check if this is a key-value pair
|
||||||
|
const colonIndex = trimmed.indexOf(':');
|
||||||
|
if (colonIndex !== -1 && !trimmed.startsWith('-')) {
|
||||||
|
// Finish previous key if we were building one
|
||||||
|
if (currentKey) {
|
||||||
|
if (inArray) {
|
||||||
|
result[currentKey] = arrayItems;
|
||||||
|
arrayItems = [];
|
||||||
|
inArray = false;
|
||||||
|
} else {
|
||||||
|
result[currentKey] = currentValue.trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
currentKey = trimmed.substring(0, colonIndex).trim();
|
||||||
|
currentValue = trimmed.substring(colonIndex + 1).trim();
|
||||||
|
|
||||||
|
// Remove quotes if present
|
||||||
|
if ((currentValue.startsWith('"') && currentValue.endsWith('"')) ||
|
||||||
|
(currentValue.startsWith("'") && currentValue.endsWith("'"))) {
|
||||||
|
currentValue = currentValue.slice(1, -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if this is an array
|
||||||
|
if (currentValue.startsWith('[') && currentValue.endsWith(']')) {
|
||||||
|
const arrayContent = currentValue.slice(1, -1);
|
||||||
|
if (arrayContent.trim()) {
|
||||||
|
result[currentKey] = arrayContent.split(',').map(item => {
|
||||||
|
item = item.trim();
|
||||||
|
// Remove quotes from array items
|
||||||
|
if ((item.startsWith('"') && item.endsWith('"')) ||
|
||||||
|
(item.startsWith("'") && item.endsWith("'"))) {
|
||||||
|
item = item.slice(1, -1);
|
||||||
|
}
|
||||||
|
return item;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
result[currentKey] = [];
|
||||||
|
}
|
||||||
|
currentKey = null;
|
||||||
|
currentValue = '';
|
||||||
|
} else if (currentValue === '' || currentValue === '[]') {
|
||||||
|
// Empty value or empty array, might be multi-line
|
||||||
|
if (currentValue === '[]') {
|
||||||
|
result[currentKey] = [];
|
||||||
|
currentKey = null;
|
||||||
|
currentValue = '';
|
||||||
|
} else {
|
||||||
|
// Check if next line starts with a dash (array item)
|
||||||
|
if (i + 1 < lines.length && lines[i + 1].trim().startsWith('-')) {
|
||||||
|
inArray = true;
|
||||||
|
arrayItems = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (trimmed.startsWith('-') && currentKey && inArray) {
|
||||||
|
// Array item
|
||||||
|
let item = trimmed.substring(1).trim();
|
||||||
|
// Remove quotes
|
||||||
|
if ((item.startsWith('"') && item.endsWith('"')) ||
|
||||||
|
(item.startsWith("'") && item.endsWith("'"))) {
|
||||||
|
item = item.slice(1, -1);
|
||||||
|
}
|
||||||
|
arrayItems.push(item);
|
||||||
|
} else if (currentKey && !inArray) {
|
||||||
|
// Multi-line value
|
||||||
|
currentValue += ' ' + trimmed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finish the last key
|
||||||
|
if (currentKey) {
|
||||||
|
if (inArray) {
|
||||||
|
result[currentKey] = arrayItems;
|
||||||
|
} else {
|
||||||
|
let finalValue = currentValue.trim();
|
||||||
|
// Remove quotes if present
|
||||||
|
if ((finalValue.startsWith('"') && finalValue.endsWith('"')) ||
|
||||||
|
(finalValue.startsWith("'") && finalValue.endsWith("'"))) {
|
||||||
|
finalValue = finalValue.slice(1, -1);
|
||||||
|
}
|
||||||
|
result[currentKey] = finalValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to extract frontmatter from a markdown file
|
||||||
|
function extractFrontmatter(filePath) {
|
||||||
|
let content = fs.readFileSync(filePath, 'utf8');
|
||||||
|
|
||||||
|
// Remove BOM if present (handles files with Byte Order Mark)
|
||||||
|
if (content.charCodeAt(0) === 0xFEFF) {
|
||||||
|
content = content.slice(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the file starts with frontmatter
|
||||||
|
if (!content.startsWith('---')) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lines = content.split('\n');
|
||||||
|
let frontmatterEnd = -1;
|
||||||
|
|
||||||
|
// Find the end of frontmatter
|
||||||
|
for (let i = 1; i < lines.length; i++) {
|
||||||
|
if (lines[i].trim() === '---') {
|
||||||
|
frontmatterEnd = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (frontmatterEnd === -1) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract frontmatter content
|
||||||
|
const frontmatterContent = lines.slice(1, frontmatterEnd).join('\n');
|
||||||
|
|
||||||
|
try {
|
||||||
|
return parseSimpleYaml(frontmatterContent);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error parsing frontmatter in ${filePath}:`, error.message);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to process files in a directory
|
||||||
|
function processDirectory(dirPath, fileExtension) {
|
||||||
|
const files = fs.readdirSync(dirPath)
|
||||||
|
.filter(file => file.endsWith(fileExtension))
|
||||||
|
.sort();
|
||||||
|
|
||||||
|
const results = [];
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
const filePath = path.join(dirPath, file);
|
||||||
|
const frontmatter = extractFrontmatter(filePath);
|
||||||
|
|
||||||
|
if (frontmatter) {
|
||||||
|
const result = {
|
||||||
|
filename: file,
|
||||||
|
...frontmatter
|
||||||
|
};
|
||||||
|
|
||||||
|
// Ensure description is present (required by schema)
|
||||||
|
if (!result.description) {
|
||||||
|
console.warn(`Warning: No description found in ${file}, adding placeholder`);
|
||||||
|
result.description = 'No description provided';
|
||||||
|
}
|
||||||
|
|
||||||
|
results.push(result);
|
||||||
|
} else {
|
||||||
|
console.warn(`Warning: No frontmatter found in ${file}, skipping`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process all directories
|
||||||
|
const metadata = {
|
||||||
|
chatmodes: processDirectory(directories.chatmodes, '.chatmode.md'),
|
||||||
|
instructions: processDirectory(directories.instructions, '.instructions.md'),
|
||||||
|
prompts: processDirectory(directories.prompts, '.prompt.md')
|
||||||
|
};
|
||||||
|
|
||||||
|
// Write the metadata.json file
|
||||||
|
const outputPath = path.join(__dirname, 'metadata.json');
|
||||||
|
fs.writeFileSync(outputPath, JSON.stringify(metadata, null, 2));
|
||||||
|
|
||||||
|
console.log(`Extracted frontmatter from ${metadata.chatmodes.length} chatmode files`);
|
||||||
|
console.log(`Extracted frontmatter from ${metadata.instructions.length} instruction files`);
|
||||||
|
console.log(`Extracted frontmatter from ${metadata.prompts.length} prompt files`);
|
||||||
|
console.log(`Metadata written to ${outputPath}`);
|
||||||
|
|
||||||
|
// Validate that required fields are present
|
||||||
|
let hasErrors = false;
|
||||||
|
|
||||||
|
// Check chatmodes
|
||||||
|
metadata.chatmodes.forEach(chatmode => {
|
||||||
|
if (!chatmode.filename || !chatmode.description) {
|
||||||
|
console.error(`Error: Chatmode missing required fields: ${chatmode.filename || 'unknown'}`);
|
||||||
|
hasErrors = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check instructions
|
||||||
|
metadata.instructions.forEach(instruction => {
|
||||||
|
if (!instruction.filename || !instruction.description) {
|
||||||
|
console.error(`Error: Instruction missing required fields: ${instruction.filename || 'unknown'}`);
|
||||||
|
hasErrors = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check prompts
|
||||||
|
metadata.prompts.forEach(prompt => {
|
||||||
|
if (!prompt.filename || !prompt.description) {
|
||||||
|
console.error(`Error: Prompt missing required fields: ${prompt.filename || 'unknown'}`);
|
||||||
|
hasErrors = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (hasErrors) {
|
||||||
|
console.error('Some files are missing required fields. Please check the output above.');
|
||||||
|
process.exit(1);
|
||||||
|
} else {
|
||||||
|
console.log('All files have required fields. Metadata extraction completed successfully.');
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user