From 2f972ba80c424f0aa9b988c306ae05e9042ddf51 Mon Sep 17 00:00:00 2001 From: Aaron Powell Date: Tue, 28 Apr 2026 17:29:40 +1000 Subject: [PATCH] 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 --- .github/workflows/publish.yml | 2 +- .github/workflows/skill-quality-report.yml | 78 ++++++ AGENTS.md | 57 ++-- {scripts => eng}/delete-gone-branches.sh | 0 {scripts => eng}/fix-line-endings.sh | 0 eng/generate-open-pr-report.mjs | 291 +++++++++++++++++++++ eng/generate-website-data.mjs | 0 eng/migrate-prompts-to-skills.mjs | 137 ---------- eng/update-plugin-commands-to-skills.mjs | 165 ------------ 9 files changed, 409 insertions(+), 321 deletions(-) rename {scripts => eng}/delete-gone-branches.sh (100%) rename {scripts => eng}/fix-line-endings.sh (100%) create mode 100755 eng/generate-open-pr-report.mjs mode change 100644 => 100755 eng/generate-website-data.mjs delete mode 100755 eng/migrate-prompts-to-skills.mjs delete mode 100755 eng/update-plugin-commands-to-skills.mjs diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 32a53809..f2dec151 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -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: | diff --git a/.github/workflows/skill-quality-report.yml b/.github/workflows/skill-quality-report.yml index 9bd036ad..75db829d 100644 --- a/.github/workflows/skill-quality-report.yml +++ b/.github/workflows/skill-quality-report.yml @@ -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 diff --git a/AGENTS.md b/AGENTS.md index 38a2f400..acb17e3b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -57,18 +57,21 @@ npm run skill:create -- --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 ` 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 diff --git a/scripts/delete-gone-branches.sh b/eng/delete-gone-branches.sh similarity index 100% rename from scripts/delete-gone-branches.sh rename to eng/delete-gone-branches.sh diff --git a/scripts/fix-line-endings.sh b/eng/fix-line-endings.sh similarity index 100% rename from scripts/fix-line-endings.sh rename to eng/fix-line-endings.sh diff --git a/eng/generate-open-pr-report.mjs b/eng/generate-open-pr-report.mjs new file mode 100755 index 00000000..1860b40b --- /dev/null +++ b/eng/generate-open-pr-report.mjs @@ -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: /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(); diff --git a/eng/generate-website-data.mjs b/eng/generate-website-data.mjs old mode 100644 new mode 100755 diff --git a/eng/migrate-prompts-to-skills.mjs b/eng/migrate-prompts-to-skills.mjs deleted file mode 100755 index 173ea5ff..00000000 --- a/eng/migrate-prompts-to-skills.mjs +++ /dev/null @@ -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(); diff --git a/eng/update-plugin-commands-to-skills.mjs b/eng/update-plugin-commands-to-skills.mjs deleted file mode 100755 index c09736ab..00000000 --- a/eng/update-plugin-commands-to-skills.mjs +++ /dev/null @@ -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();