diff --git a/.github/extensions/external-plugins-board/extension.mjs b/.github/extensions/external-plugins-board/extension.mjs new file mode 100644 index 00000000..1896ec03 --- /dev/null +++ b/.github/extensions/external-plugins-board/extension.mjs @@ -0,0 +1,580 @@ +import { createServer } from "node:http"; +import { execFileSync, spawnSync, execSync } from "node:child_process"; +import { dirname } from "node:path"; +import { createRequire } from "node:module"; +import { joinSession, createCanvas } from "@github/copilot-sdk/extension"; + +const require = createRequire(import.meta.url); +const { marked } = require("marked"); + +const servers = new Map(); +let workspacePath = null; +let lastError = null; + +// Fetch live issues from GitHub REST API instead of gh CLI subprocess +async function fetchLiveIssues(cwd) { + try { + // Use GitHub REST API to fetch issues + // This avoids the subprocess execution restriction + const owner = "github"; + const repo = "awesome-copilot"; + const label = "external-plugin"; + + // Get authentication token from environment or use public access + const token = process.env.GITHUB_TOKEN || process.env.GH_TOKEN; + + const headers = { + "Accept": "application/vnd.github.v3+json" + }; + + if (token) { + headers["Authorization"] = `token ${token}`; + } + + // Fetch issues with external-plugin label + const response = await fetch( + `https://api.github.com/repos/${owner}/${repo}/issues?labels=${label}&state=open&per_page=100`, + { headers } + ); + + if (!response.ok) { + const error = await response.text(); + throw new Error(`GitHub API error ${response.status}: ${error.substring(0, 200)}`); + } + + const issues = await response.json(); + + // Filter to only external-plugin labeled issues and map to our format + return issues + .filter(issue => issue.labels && issue.labels.some(l => l.name === label)) + .map(issue => ({ + number: issue.number, + title: issue.title, + body: issue.body || "", + bodyHtml: marked.parse(issue.body || ""), + labels: (issue.labels || []).map(l => ({ name: l.name })), + pr_url: issue.body?.match(/\[Generated PR\]\(([^)]+)\)/)?.[1], + created_at: issue.created_at, + updated_at: issue.updated_at + })); + } catch (err) { + lastError = err.message; + throw err; + } +} + +function renderHtml() { + return ` + + + + External Plugins Board + + + +

External Plugins Board

+
Loading issues...
+ + + + + +`; +} + +async function startServer(instanceId, cwd) { + const server = createServer(async (req, res) => { + res.setHeader("Access-Control-Allow-Origin", "*"); + + if (req.url === "/" && req.method === "GET") { + res.setHeader("Content-Type", "text/html; charset=utf-8"); + res.end(renderHtml()); + } else if (req.url === "/api/issues" && req.method === "GET") { + try { + const issues = await fetchLiveIssues(cwd); + res.setHeader("Content-Type", "application/json"); + res.end(JSON.stringify(issues || [])); + } catch (err) { + res.writeHead(500, { "Content-Type": "application/json" }); + res.end(JSON.stringify({ error: err.message })); + } + } else if (req.url === "/api/issues/update" && req.method === "POST") { + let body = ""; + req.on("data", chunk => { body += chunk; }); + req.on("end", async () => { + try { + const { issueNumber, newState } = JSON.parse(body); + const labels = ['requires-submitter-fixes', 'ready-for-review', 'approved', 'rejected']; + for (const label of labels.filter(l => l !== newState)) { + try { + spawnSync("gh", [ + "issue", "edit", issueNumber.toString(), + "--remove-label", label + ], { cwd, shell: true }); + } catch (e) {} + } + spawnSync("gh", [ + "issue", "edit", issueNumber.toString(), + "--add-label", newState + ], { cwd, shell: true }); + res.setHeader("Content-Type", "application/json"); + res.end(JSON.stringify({ ok: true })); + } catch (err) { + res.writeHead(500, { "Content-Type": "application/json" }); + res.end(JSON.stringify({ error: err.message })); + } + }); + } else { + res.writeHead(404); + res.end("Not found"); + } + }); + + await new Promise(resolve => server.listen(0, "127.0.0.1", resolve)); + const port = server.address().port; + return { server, url: `http://127.0.0.1:${port}/` }; +} + +const session = await joinSession({ + canvases: [ + createCanvas({ + id: "external-plugins-board", + displayName: "External Plugins Board", + description: "Kanban board for managing external plugin submission issues", + open: async (ctx) => { + let entry = servers.get(ctx.instanceId); + if (!entry) { + if (!workspacePath) { + const filePath = import.meta.url.replace(/^file:\/\//, '').replace(/\//g, '\\'); + workspacePath = dirname(dirname(dirname(filePath))); + } + entry = await startServer(ctx.instanceId, workspacePath); + servers.set(ctx.instanceId, entry); + } + return { title: "External Plugins Board", url: entry.url }; + }, + onClose: async (ctx) => { + const entry = servers.get(ctx.instanceId); + if (entry) { + servers.delete(ctx.instanceId); + await new Promise(resolve => entry.server.close(() => resolve())); + } + }, + }), + ], +}); diff --git a/.github/extensions/external-plugins-board/package-lock.json b/.github/extensions/external-plugins-board/package-lock.json new file mode 100644 index 00000000..749f14a6 --- /dev/null +++ b/.github/extensions/external-plugins-board/package-lock.json @@ -0,0 +1,27 @@ +{ + "name": "external-plugins-board", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "external-plugins-board", + "version": "1.0.0", + "dependencies": { + "marked": "^15.0.0" + } + }, + "node_modules/marked": { + "version": "15.0.12", + "resolved": "https://registry.npmjs.org/marked/-/marked-15.0.12.tgz", + "integrity": "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==", + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 18" + } + } + } +} diff --git a/.github/extensions/external-plugins-board/package.json b/.github/extensions/external-plugins-board/package.json new file mode 100644 index 00000000..495cf54f --- /dev/null +++ b/.github/extensions/external-plugins-board/package.json @@ -0,0 +1,8 @@ +{ + "name": "external-plugins-board", + "version": "1.0.0", + "type": "module", + "dependencies": { + "marked": "^15.0.0" + } +}