Consolidate scripts and automate report management (#1540)

* removing old scripts

* consolidated folder

* Updating usage of scripts

* Adding script to generate an open PR report, rather than making AI gen it each time

* Adding step to close old quality report discussions
This commit is contained in:
Aaron Powell
2026-04-28 17:29:40 +10:00
committed by GitHub
parent f7a7ef7c28
commit 2f972ba80c
9 changed files with 409 additions and 321 deletions

View File

@@ -43,7 +43,7 @@ jobs:
run: npm run build
- name: Fix line endings
run: bash scripts/fix-line-endings.sh
run: bash eng/fix-line-endings.sh
- name: Publish to main
run: |

View File

@@ -297,6 +297,84 @@ jobs:
}
core.setOutput('comment_count', String(commentParts.length));
- name: Close old report discussions
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0
with:
script: |
const RETENTION_DAYS = 5;
const CATEGORY_NAME = 'Skill Quality Reports';
const TITLE_PREFIX = 'Skill Quality Report — ';
const cutoff = new Date();
cutoff.setUTCDate(cutoff.getUTCDate() - RETENTION_DAYS);
let after = null;
let closedCount = 0;
let reachedCutoff = false;
while (!reachedCutoff) {
const result = await github.graphql(`
query($owner: String!, $repo: String!, $after: String) {
repository(owner: $owner, name: $repo) {
discussions(first: 100, after: $after, orderBy: { field: CREATED_AT, direction: ASC }) {
nodes {
id
title
createdAt
closed
url
category {
name
}
}
pageInfo {
hasNextPage
endCursor
}
}
}
}
`, {
owner: context.repo.owner,
repo: context.repo.repo,
after,
});
const discussions = result.repository.discussions;
for (const discussion of discussions.nodes) {
if (new Date(discussion.createdAt) >= cutoff) {
reachedCutoff = true;
break;
}
if (discussion.category?.name !== CATEGORY_NAME) continue;
if (!discussion.title.startsWith(TITLE_PREFIX)) continue;
if (discussion.closed) continue;
await github.graphql(`
mutation($discussionId: ID!) {
closeDiscussion(input: { discussionId: $discussionId }) {
discussion {
id
}
}
}
`, { discussionId: discussion.id });
closedCount++;
console.log(`Closed old report discussion: ${discussion.url}`);
}
if (reachedCutoff || !discussions.pageInfo.hasNextPage) {
break;
}
after = discussions.pageInfo.endCursor;
}
console.log(`Closed ${closedCount} report discussion(s) older than ${RETENTION_DAYS} days.`);
# ── Create Discussion (preferred) or Issue (fallback) ────────
- name: Create Discussion
id: create-discussion

View File

@@ -57,18 +57,21 @@ npm run skill:create -- --name <skill-name>
All agent files (`*.agent.md`) and instruction files (`*.instructions.md`) must include proper markdown front matter. Agent Skills are folders containing a `SKILL.md` file with frontmatter and optional bundled assets. Hooks are folders containing a `README.md` with frontmatter and a `hooks.json` configuration file:
#### Agent Files (*.agent.md)
#### Agent Files (\*.agent.md)
- Must have `description` field (wrapped in single quotes)
- File names should be lower case with words separated by hyphens
- Recommended to include `tools` field
- Strongly recommended to specify `model` field
#### Instruction Files (*.instructions.md)
#### Instruction Files (\*.instructions.md)
- Must have `description` field (wrapped in single quotes, not empty)
- Must have `applyTo` field specifying file patterns (e.g., `'**.js, **.ts'`)
- File names should be lower case with words separated by hyphens
#### Agent Skills (skills/*/SKILL.md)
#### Agent Skills (skills/\*/SKILL.md)
- Each skill is a folder containing a `SKILL.md` file
- SKILL.md must have `name` field (lowercase with hyphens, matching folder name, max 64 characters)
- SKILL.md must have `description` field (wrapped in single quotes, 10-1024 characters)
@@ -78,7 +81,8 @@ All agent files (`*.agent.md`) and instruction files (`*.instructions.md`) must
- Asset files should be reasonably sized (under 5MB per file)
- Skills follow the [Agent Skills specification](https://agentskills.io/specification)
#### Hook Folders (hooks/*/README.md)
#### Hook Folders (hooks/\*/README.md)
- Each hook is a folder containing a `README.md` file with frontmatter
- README.md must have `name` field (human-readable name)
- README.md must have `description` field (wrapped in single quotes, not empty)
@@ -89,7 +93,8 @@ All agent files (`*.agent.md`) and instruction files (`*.instructions.md`) must
- Follow the [GitHub Copilot hooks specification](https://docs.github.com/en/copilot/how-tos/use-copilot-agents/coding-agent/use-hooks)
- Optionally includes `tags` field for categorization
#### Workflow Files (workflows/*.md)
#### Workflow Files (workflows/\*.md)
- Each workflow is a standalone `.md` file in the `workflows/` directory
- Must have `name` field (human-readable name)
- Must have `description` field (wrapped in single quotes, not empty)
@@ -98,7 +103,8 @@ All agent files (`*.agent.md`) and instruction files (`*.instructions.md`) must
- Only `.md` files are accepted — `.yml`, `.yaml`, and `.lock.yml` files are blocked by CI
- Follow the [GitHub Agentic Workflows specification](https://github.github.com/gh-aw/reference/workflow-structure/)
#### Plugin Folders (plugins/*)
#### Plugin Folders (plugins/\*)
- Each plugin is a folder containing a `.github/plugin/plugin.json` file with metadata
- plugin.json must have `name` field (matching the folder name)
- plugin.json must have `description` field (describing the plugin's purpose)
@@ -112,12 +118,14 @@ All agent files (`*.agent.md`) and instruction files (`*.instructions.md`) must
When adding a new agent, instruction, skill, hook, workflow, or plugin:
**For Agents and Instructions:**
1. Create the file with proper front matter
2. Add the file to the appropriate directory
3. Update the README.md by running: `npm run build`
4. Verify the resource appears in the generated README
**For Hooks:**
1. Create a new folder in `hooks/` with a descriptive name
2. Create `README.md` with proper frontmatter (name, description, hooks, tags)
3. Create `hooks.json` with hook configuration following GitHub Copilot hooks spec
@@ -126,16 +134,16 @@ When adding a new agent, instruction, skill, hook, workflow, or plugin:
6. Update the README.md by running: `npm run build`
7. Verify the hook appears in the generated README
**For Workflows:**
1. Create a new `.md` file in `workflows/` with a descriptive name (e.g., `daily-issues-report.md`)
2. Include frontmatter with `name` and `description`, plus agentic workflow fields (`on`, `permissions`, `safe-outputs`)
3. Compile with `gh aw compile --validate` to verify it's valid
4. Update the README.md by running: `npm run build`
5. Verify the workflow appears in the generated README
**For Skills:**
1. Run `npm run skill:create` to scaffold a new skill folder
2. Edit the generated SKILL.md file with your instructions
3. Add any bundled assets (scripts, templates, data) to the skill folder
@@ -144,6 +152,7 @@ When adding a new agent, instruction, skill, hook, workflow, or plugin:
6. Verify the skill appears in the generated README
**For Plugins:**
1. Run `npm run plugin:create -- --name <plugin-name>` to scaffold a new plugin
2. Define agents, commands, and skills in `plugin.json` using Claude Code spec fields
3. Edit the generated `plugin.json` with your metadata
@@ -152,6 +161,7 @@ When adding a new agent, instruction, skill, hook, workflow, or plugin:
6. Verify the plugin appears in `.github/plugin/marketplace.json`
**For External Plugins:**
1. Edit `plugins/external.json` and add an entry with `name`, `source`, `description`, and `version`
2. The `source` field should be an object specifying a GitHub repo, git URL, npm package, or pip package (see [CONTRIBUTING.md](CONTRIBUTING.md#adding-external-plugins))
3. Run `npm run build` to regenerate marketplace.json
@@ -168,25 +178,28 @@ npm run skill:validate
npm run build
# Fix line endings (required before committing)
bash scripts/fix-line-endings.sh
bash eng/fix-line-endings.sh
```
Before committing:
- Ensure all markdown front matter is correctly formatted
- Verify file names follow the lower-case-with-hyphens convention
- Run `npm run build` to update the README
- **Always run `bash scripts/fix-line-endings.sh`** to normalize line endings (CRLF → LF)
- **Always run `bash eng/fix-line-endings.sh`** to normalize line endings (CRLF → LF)
- Check that your new resource appears correctly in the README
## Code Style Guidelines
### Markdown Files
- Use proper front matter with required fields
- Keep descriptions concise and informative
- Wrap description field values in single quotes
- Use lower-case file names with hyphens as separators
### JavaScript/Node.js Scripts
- Located in `eng/` and `scripts/` directories
- Follow Node.js ES module conventions (`.mjs` extension)
- Use clear, descriptive function and variable names
@@ -201,29 +214,32 @@ When creating a pull request:
2. **Front matter validation**: Ensure all markdown files have the required front matter fields
3. **File naming**: Verify all new files follow the lower-case-with-hyphens naming convention
4. **Build check**: Run `npm run build` before committing to verify README generation
5. **Line endings**: **Always run `bash scripts/fix-line-endings.sh`** to normalize line endings to LF (Unix-style)
5. **Line endings**: **Always run `bash eng/fix-line-endings.sh`** to normalize line endings to LF (Unix-style)
6. **Description**: Provide a clear description of what your agent/instruction does
7. **Testing**: If adding a plugin, run `npm run plugin:validate` to ensure validity
### Pre-commit Checklist
Before submitting your PR, ensure you have:
- [ ] Run `npm install` (or `npm ci`) to install dependencies
- [ ] Run `npm run build` to generate the updated README.md
- [ ] Run `bash scripts/fix-line-endings.sh` to normalize line endings
- [ ] Run `bash eng/fix-line-endings.sh` to normalize line endings
- [ ] Verified that all new files have proper front matter
- [ ] Tested that your contribution works with GitHub Copilot
- [ ] Checked that file names follow the naming convention
### Code Review Checklist
For instruction files (*.instructions.md):
For instruction files (\*.instructions.md):
- [ ] Has markdown front matter
- [ ] Has non-empty `description` field wrapped in single quotes
- [ ] Has `applyTo` field with file patterns
- [ ] File name is lower case with hyphens
For agent files (*.agent.md):
For agent files (\*.agent.md):
- [ ] Has markdown front matter
- [ ] Has non-empty `description` field wrapped in single quotes
- [ ] Has `name` field with human-readable name (e.g., "Address Comments" not "address-comments")
@@ -231,7 +247,8 @@ For agent files (*.agent.md):
- [ ] Includes `model` field (strongly recommended)
- [ ] Considers using `tools` field
For skills (skills/*/):
For skills (skills/\*/):
- [ ] Folder contains a SKILL.md file
- [ ] SKILL.md has markdown front matter
- [ ] Has `name` field matching folder name (lowercase with hyphens, max 64 characters)
@@ -240,7 +257,8 @@ For skills (skills/*/):
- [ ] Any bundled assets are referenced in SKILL.md
- [ ] Bundled assets are under 5MB per file
For hook folders (hooks/*/):
For hook folders (hooks/\*/):
- [ ] Folder contains a README.md file with markdown front matter
- [ ] Has `name` field with human-readable name
- [ ] Has non-empty `description` field wrapped in single quotes
@@ -250,7 +268,8 @@ For hook folders (hooks/*/):
- [ ] Follows [GitHub Copilot hooks specification](https://docs.github.com/en/copilot/how-tos/use-copilot-agents/coding-agent/use-hooks)
- [ ] Optionally includes `tags` array field for categorization
For workflow files (workflows/*.md):
For workflow files (workflows/\*.md):
- [ ] File has markdown front matter
- [ ] Has `name` field with human-readable name
- [ ] Has non-empty `description` field wrapped in single quotes
@@ -260,7 +279,8 @@ For workflow files (workflows/*.md):
- [ ] No `.yml`, `.yaml`, or `.lock.yml` files included
- [ ] Follows [GitHub Agentic Workflows specification](https://github.github.com/gh-aw/reference/workflow-structure/)
For plugins (plugins/*/):
For plugins (plugins/\*/):
- [ ] Directory contains a `.github/plugin/plugin.json` file
- [ ] Directory contains a `README.md` file
- [ ] `plugin.json` has `name` field matching the directory name (lowercase with hyphens)
@@ -275,6 +295,7 @@ For plugins (plugins/*/):
## Contributing
This is a community-driven project. Contributions are welcome! Please see:
- [CONTRIBUTING.md](CONTRIBUTING.md) for contribution guidelines
- [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md) for community standards
- [SECURITY.md](SECURITY.md) for security policies

291
eng/generate-open-pr-report.mjs Executable file
View File

@@ -0,0 +1,291 @@
#!/usr/bin/env node
import { execFileSync } from "node:child_process";
import fs from "node:fs";
import path from "node:path";
import { ROOT_FOLDER } from "./constants.mjs";
import { setupGracefulShutdown } from "./utils/graceful-shutdown.mjs";
const DEFAULT_REPO = "github/awesome-copilot";
const DEFAULT_LIMIT = 500;
const DEFAULT_CMD_TIMEOUT = 30_000;
const REPORT_DEFINITIONS = [
{
heading: "PRs that target `main`",
fileName: "prs-targeting-main.json",
predicate: (pr) => pr.targetBranch === "main"
},
{
heading: "PRs that target `staged` which are passing all checks and have less than 10 files",
fileName: "prs-staged-passing-under-10-files.json",
predicate: (pr) => pr.targetBranch === "staged" && pr.checksPass && pr.fileCount < 10
},
{
heading: "PRs that target `staged` which have between 10 and 50 files",
fileName: "prs-staged-10-to-50-files.json",
predicate: (pr) => pr.targetBranch === "staged" && pr.fileCount >= 10 && pr.fileCount <= 50
},
{
heading: "PRs that target `staged` with greater than 50 files",
fileName: "prs-staged-over-50-files.json",
predicate: (pr) => pr.targetBranch === "staged" && pr.fileCount > 50
}
];
setupGracefulShutdown("generate-open-pr-report");
function printUsage() {
console.log(`Usage: node eng/generate-open-pr-report.mjs [--repo owner/name] [--output-dir path] [--limit N]
Generate open PR reports for a GitHub repository.
Outputs:
- open-pr-report.md
- prs-targeting-main.json
- prs-staged-passing-under-10-files.json
- prs-staged-10-to-50-files.json
- prs-staged-over-50-files.json
Options:
--repo GitHub repository in owner/name format (default: ${DEFAULT_REPO})
--output-dir Directory for generated reports (default: <repo-root>/reports)
--limit Max number of open PRs to fetch (default: ${DEFAULT_LIMIT})
--help, -h Show this help text`);
}
function parseArgs(argv) {
const options = {
repo: DEFAULT_REPO,
outputDir: path.join(ROOT_FOLDER, "reports"),
limit: DEFAULT_LIMIT
};
for (let i = 0; i < argv.length; i += 1) {
const arg = argv[i];
if (arg === "--help" || arg === "-h") {
printUsage();
process.exit(0);
}
if (arg === "--repo") {
options.repo = argv[i + 1] ?? "";
i += 1;
continue;
}
if (arg === "--output-dir") {
options.outputDir = argv[i + 1] ?? "";
i += 1;
continue;
}
if (arg === "--limit") {
options.limit = Number.parseInt(argv[i + 1] ?? "", 10);
i += 1;
continue;
}
throw new Error(`Unknown option: ${arg}`);
}
if (!options.repo || !options.repo.includes("/")) {
throw new Error("--repo must be in owner/name format.");
}
if (!Number.isInteger(options.limit) || options.limit < 1) {
throw new Error("--limit must be a positive integer.");
}
if (!options.outputDir) {
throw new Error("--output-dir is required.");
}
return options;
}
function ensureCommandAvailable(command) {
try {
execFileSync(command, ["--version"], {
stdio: "ignore",
timeout: DEFAULT_CMD_TIMEOUT
});
} catch (error) {
throw new Error(`Missing required command: ${command}`);
}
}
function runGhJson(args) {
const output = execFileSync("gh", args, {
encoding: "utf8",
stdio: ["ignore", "pipe", "pipe"],
timeout: DEFAULT_CMD_TIMEOUT
});
return JSON.parse(output);
}
function getCheckState(statusCheckRollup) {
if (!Array.isArray(statusCheckRollup) || statusCheckRollup.length === 0) {
return "NONE";
}
if (statusCheckRollup.some((check) => check.status !== "COMPLETED")) {
return "PENDING";
}
const failureConclusions = new Set([
"FAILURE",
"TIMED_OUT",
"ACTION_REQUIRED",
"CANCELLED",
"STALE",
"STARTUP_FAILURE"
]);
if (statusCheckRollup.some((check) => failureConclusions.has(check.conclusion ?? ""))) {
return "FAILURE";
}
const successConclusions = new Set(["SUCCESS", "NEUTRAL", "SKIPPED"]);
const allSuccessful = statusCheckRollup.every((check) => successConclusions.has(check.conclusion ?? ""));
return allSuccessful ? "SUCCESS" : "FAILURE";
}
function normalizePullRequest(pr) {
const checkState = getCheckState(pr.statusCheckRollup);
return {
id: pr.number,
title: pr.title,
author: pr.author?.login ?? "ghost",
checksPass: checkState === "SUCCESS",
checkState,
targetBranch: pr.baseRefName,
fileCount: pr.changedFiles,
createdAt: pr.createdAt,
updatedAt: pr.updatedAt,
createdAgeDays: getAgeInDays(pr.createdAt),
updatedAgeDays: getAgeInDays(pr.updatedAt),
url: pr.url
};
}
function getCheckLabel(pr) {
if (pr.checkState === "SUCCESS") {
return "Yes";
}
if (pr.checkState === "PENDING") {
return "Pending";
}
if (pr.checkState === "NONE") {
return "No checks";
}
return "No";
}
function escapeMarkdownCell(value) {
return String(value).replaceAll("|", "\\|");
}
function getAgeInDays(timestamp) {
const milliseconds = Date.now() - new Date(timestamp).getTime();
return Math.max(0, Math.floor(milliseconds / (24 * 60 * 60 * 1000)));
}
function formatTimestampWithAge(timestamp) {
return `${timestamp.slice(0, 10)} (${getAgeInDays(timestamp)}d ago)`;
}
function renderTable(prs) {
const lines = [
"| PR title + ID | Author | Whether checks pass | Created | Updated | Link to PR |",
"| --- | --- | --- | --- | --- | --- |"
];
if (prs.length === 0) {
lines.push("| None | - | - | - | - | - |");
return lines.join("\n");
}
for (const pr of prs) {
lines.push(
`| ${escapeMarkdownCell(pr.title)} (#${pr.id}) | ${escapeMarkdownCell(pr.author)} | ${getCheckLabel(pr)} | ${formatTimestampWithAge(pr.createdAt)} | ${formatTimestampWithAge(pr.updatedAt)} | [Link](${pr.url}) |`
);
}
return lines.join("\n");
}
function renderMarkdownReport(repo, generatedAt, categorizedReports) {
const sections = [
"# Open PR report",
"",
`**Repository:** \`${repo}\` `,
`**Generated:** \`${generatedAt}\``
];
for (const report of categorizedReports) {
sections.push("", `## ${report.heading}`, "", renderTable(report.items));
}
return `${sections.join("\n")}\n`;
}
function writeJsonReport(filePath, items) {
fs.writeFileSync(filePath, `${JSON.stringify(items, null, 2)}\n`);
}
function generateOpenPrReport() {
const options = parseArgs(process.argv.slice(2));
ensureCommandAvailable("gh");
console.log(`Fetching open PRs from ${options.repo}...`);
const pullRequests = runGhJson([
"pr",
"list",
"--repo",
options.repo,
"--state",
"open",
"--limit",
String(options.limit),
"--json",
"number,title,url,author,baseRefName,changedFiles,createdAt,updatedAt,statusCheckRollup"
]);
const normalizedPullRequests = pullRequests.map(normalizePullRequest);
const categorizedReports = REPORT_DEFINITIONS.map((report) => ({
...report,
items: normalizedPullRequests.filter(report.predicate)
}));
fs.mkdirSync(options.outputDir, { recursive: true });
for (const report of categorizedReports) {
writeJsonReport(path.join(options.outputDir, report.fileName), report.items);
}
const markdownReport = renderMarkdownReport(
options.repo,
new Date().toISOString(),
categorizedReports
);
const markdownFilePath = path.join(options.outputDir, "open-pr-report.md");
fs.writeFileSync(markdownFilePath, markdownReport);
console.log(`Generated reports in ${options.outputDir}:`);
console.log(" open-pr-report.md");
for (const report of categorizedReports) {
console.log(` ${report.fileName}`);
}
}
generateOpenPrReport();

0
eng/generate-website-data.mjs Normal file → Executable file
View File

View File

@@ -1,137 +0,0 @@
#!/usr/bin/env node
import fs from "fs";
import path from "path";
import { ROOT_FOLDER, SKILLS_DIR } from "./constants.mjs";
import { parseFrontmatter } from "./yaml-parser.mjs";
const PROMPTS_DIR = path.join(ROOT_FOLDER, "prompts");
/**
* Convert a prompt file to a skill folder
* @param {string} promptFilePath - Full path to the prompt file
* @returns {object} Result with success status and details
*/
function convertPromptToSkill(promptFilePath) {
const filename = path.basename(promptFilePath);
const baseName = filename.replace(".prompt.md", "");
console.log(`\nConverting: ${baseName}`);
// Parse the prompt file frontmatter
const frontmatter = parseFrontmatter(promptFilePath);
const content = fs.readFileSync(promptFilePath, "utf8");
// Extract the content after frontmatter
const frontmatterEndMatch = content.match(/^---\n[\s\S]*?\n---\n/);
const mainContent = frontmatterEndMatch
? content.substring(frontmatterEndMatch[0].length).trim()
: content.trim();
// Create skill folder
const skillFolderPath = path.join(SKILLS_DIR, baseName);
if (fs.existsSync(skillFolderPath)) {
console.log(` ⚠️ Skill folder already exists: ${baseName}`);
return { success: false, reason: "already-exists", name: baseName };
}
fs.mkdirSync(skillFolderPath, { recursive: true });
// Build new frontmatter for SKILL.md
const skillFrontmatter = {
name: baseName,
description: frontmatter?.description || `Skill converted from ${filename}`,
};
// Build SKILL.md content
const skillContent = `---
name: ${skillFrontmatter.name}
description: '${skillFrontmatter.description.replace(/'/g, "'''")}'
---
${mainContent}
`;
// Write SKILL.md
const skillFilePath = path.join(skillFolderPath, "SKILL.md");
fs.writeFileSync(skillFilePath, skillContent, "utf8");
console.log(` ✓ Created skill: ${baseName}`);
return { success: true, name: baseName, path: skillFolderPath };
}
/**
* Main migration function
*/
function main() {
console.log("=".repeat(60));
console.log("Starting Prompt to Skills Migration");
console.log("=".repeat(60));
// Check if prompts directory exists
if (!fs.existsSync(PROMPTS_DIR)) {
console.error(`Error: Prompts directory not found: ${PROMPTS_DIR}`);
process.exit(1);
}
// Get all prompt files
const promptFiles = fs
.readdirSync(PROMPTS_DIR)
.filter((file) => file.endsWith(".prompt.md"))
.map((file) => path.join(PROMPTS_DIR, file));
console.log(`Found ${promptFiles.length} prompt files to convert\n`);
const results = {
success: [],
alreadyExists: [],
failed: [],
};
// Convert each prompt
for (const promptFile of promptFiles) {
try {
const result = convertPromptToSkill(promptFile);
if (result.success) {
results.success.push(result.name);
} else if (result.reason === "already-exists") {
results.alreadyExists.push(result.name);
} else {
results.failed.push(result.name);
}
} catch (error) {
const baseName = path.basename(promptFile, ".prompt.md");
console.error(` ✗ Error converting ${baseName}: ${error.message}`);
results.failed.push(baseName);
}
}
// Print summary
console.log("\n" + "=".repeat(60));
console.log("Migration Summary");
console.log("=".repeat(60));
console.log(`✓ Successfully converted: ${results.success.length}`);
console.log(`⚠ Already existed: ${results.alreadyExists.length}`);
console.log(`✗ Failed: ${results.failed.length}`);
console.log(`Total processed: ${promptFiles.length}`);
if (results.failed.length > 0) {
console.log("\nFailed conversions:");
results.failed.forEach((name) => console.log(` - ${name}`));
}
if (results.alreadyExists.length > 0) {
console.log("\nSkipped (already exist):");
results.alreadyExists.forEach((name) => console.log(` - ${name}`));
}
console.log("\n✅ Migration complete!");
console.log(
"\nNext steps:\n" +
"1. Run 'npm run skill:validate' to validate all new skills\n" +
"2. Update plugin manifests to reference skills instead of commands\n" +
"3. Remove prompts directory after testing\n"
);
}
// Run migration
main();

View File

@@ -1,165 +0,0 @@
#!/usr/bin/env node
import fs from "fs";
import path from "path";
import { PLUGINS_DIR } from "./constants.mjs";
/**
* Convert commands references to skills references in a plugin.json
* @param {string} pluginJsonPath - Path to the plugin.json file
* @returns {object} Result with success status and details
*/
function updatePluginManifest(pluginJsonPath) {
const pluginDir = path.dirname(path.dirname(path.dirname(pluginJsonPath)));
const pluginName = path.basename(pluginDir);
console.log(`\nProcessing plugin: ${pluginName}`);
// Read and parse plugin.json
let plugin;
try {
const content = fs.readFileSync(pluginJsonPath, "utf8");
plugin = JSON.parse(content);
} catch (error) {
console.log(` ✗ Error reading/parsing: ${error.message}`);
return { success: false, name: pluginName, reason: "parse-error" };
}
// Check if plugin has commands field
if (!plugin.commands || !Array.isArray(plugin.commands)) {
console.log(` No commands field found`);
return { success: false, name: pluginName, reason: "no-commands" };
}
const commandCount = plugin.commands.length;
console.log(` Found ${commandCount} command(s) to convert`);
// Validate and convert commands to skills format
// Commands: "./commands/foo.md" → Skills: "./skills/foo/"
const validCommands = plugin.commands.filter((cmd) => {
if (typeof cmd !== "string") {
console.log(` ⚠ Skipping non-string command entry: ${JSON.stringify(cmd)}`);
return false;
}
if (!cmd.startsWith("./commands/") || !cmd.endsWith(".md")) {
console.log(` ⚠ Skipping command with unexpected format: ${cmd}`);
return false;
}
return true;
});
const skills = validCommands.map((cmd) => {
const basename = path.basename(cmd, ".md");
return `./skills/${basename}/`;
});
// Initialize skills array if it doesn't exist or is not an array
if (!Array.isArray(plugin.skills)) {
plugin.skills = [];
}
// Add converted commands to skills array, de-duplicating entries
const allSkills = new Set(plugin.skills);
for (const skillPath of skills) {
allSkills.add(skillPath);
}
plugin.skills = Array.from(allSkills);
// Remove commands field
delete plugin.commands;
// Write updated plugin.json
try {
fs.writeFileSync(
pluginJsonPath,
JSON.stringify(plugin, null, 2) + "\n",
"utf8"
);
console.log(` ✓ Converted ${commandCount} command(s) to skills`);
return { success: true, name: pluginName, count: commandCount };
} catch (error) {
console.log(` ✗ Error writing file: ${error.message}`);
return { success: false, name: pluginName, reason: "write-error" };
}
}
/**
* Main function to update all plugin manifests
*/
function main() {
console.log("=".repeat(60));
console.log("Updating Plugin Manifests: Commands → Skills");
console.log("=".repeat(60));
// Check if plugins directory exists
if (!fs.existsSync(PLUGINS_DIR)) {
console.error(`Error: Plugins directory not found: ${PLUGINS_DIR}`);
process.exit(1);
}
// Find all plugin.json files
const pluginDirs = fs
.readdirSync(PLUGINS_DIR, { withFileTypes: true })
.filter((entry) => entry.isDirectory())
.map((entry) => entry.name);
console.log(`Found ${pluginDirs.length} plugin directory(ies)\n`);
const results = {
updated: [],
noCommands: [],
failed: [],
};
// Process each plugin
for (const dirName of pluginDirs) {
const pluginJsonPath = path.join(
PLUGINS_DIR,
dirName,
".github/plugin",
"plugin.json"
);
if (!fs.existsSync(pluginJsonPath)) {
console.log(`\nSkipping ${dirName}: no plugin.json found`);
continue;
}
const result = updatePluginManifest(pluginJsonPath);
if (result.success) {
results.updated.push({ name: result.name, count: result.count });
} else if (result.reason === "no-commands") {
results.noCommands.push(result.name);
} else {
results.failed.push(result.name);
}
}
// Print summary
console.log("\n" + "=".repeat(60));
console.log("Update Summary");
console.log("=".repeat(60));
console.log(`✓ Updated plugins: ${results.updated.length}`);
console.log(` No commands field: ${results.noCommands.length}`);
console.log(`✗ Failed: ${results.failed.length}`);
console.log(`Total processed: ${pluginDirs.length}`);
if (results.updated.length > 0) {
console.log("\nUpdated plugins:");
results.updated.forEach(({ name, count }) =>
console.log(` - ${name} (${count} command(s) → skills)`)
);
}
if (results.failed.length > 0) {
console.log("\nFailed updates:");
results.failed.forEach((name) => console.log(` - ${name}`));
}
console.log("\n✅ Plugin manifest updates complete!");
console.log(
"\nNext steps:\n" +
"1. Run 'npm run plugin:validate' to validate all updated plugins\n" +
"2. Test that plugins work correctly\n"
);
}
// Run the update
main();