mirror of
https://github.com/github/awesome-copilot.git
synced 2026-06-13 11:33:32 +00:00
chore: publish from staged
This commit is contained in:
@@ -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 `<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>External Plugins Board</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body {
|
||||
background: var(--background-color-default, #ffffff);
|
||||
color: var(--text-color-default, #1f2328);
|
||||
font-family: var(--font-sans, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif);
|
||||
font-size: var(--text-body-medium, 14px);
|
||||
line-height: var(--leading-body-medium, 20px);
|
||||
padding: 1.5rem;
|
||||
}
|
||||
h1 {
|
||||
font-size: var(--text-title-medium, 20px);
|
||||
font-weight: var(--font-weight-semibold, 600);
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
.board {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 1.5rem;
|
||||
}
|
||||
.column {
|
||||
background: var(--background-color-secondary, #f6f8fa);
|
||||
border: 1px solid var(--border-color-default, #d0d7de);
|
||||
border-radius: 6px;
|
||||
padding: 1rem;
|
||||
}
|
||||
.column-header {
|
||||
font-weight: 600;
|
||||
margin-bottom: 1rem;
|
||||
padding-bottom: 0.5rem;
|
||||
border-bottom: 2px solid var(--border-color-default, #d0d7de);
|
||||
}
|
||||
.column-header.requires { color: #cf222e; }
|
||||
.column-header.ready { color: #0969da; }
|
||||
.column-header.approved { color: #1a7f34; }
|
||||
.column-header.rejected { color: var(--text-color-muted, #656d76); }
|
||||
.issues { display: flex; flex-direction: column; gap: 0.75rem; }
|
||||
.issue-card {
|
||||
background: white;
|
||||
border: 1px solid var(--border-color-default, #d0d7de);
|
||||
border-radius: 6px;
|
||||
padding: 0.75rem;
|
||||
cursor: grab;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.issue-card:hover {
|
||||
border-color: #0969da;
|
||||
box-shadow: 0 0 8px rgba(9, 105, 218, 0.2);
|
||||
}
|
||||
.issue-card.dragging { opacity: 0.5; }
|
||||
.issue-number { font-weight: 600; color: var(--text-color-muted, #656d76); font-size: 12px; }
|
||||
.issue-title { margin: 0.25rem 0; font-size: 12px; }
|
||||
.issue-link { color: #0969da; text-decoration: none; margin-top: 0.5rem; font-size: 12px; }
|
||||
.issue-link:hover { text-decoration: underline; }
|
||||
.loading { text-align: center; padding: 2rem; color: var(--text-color-muted, #656d76); }
|
||||
.error {
|
||||
color: #cf222e;
|
||||
padding: 1.5rem;
|
||||
background: var(--background-color-secondary, #f6f8fa);
|
||||
border: 1px solid #da3633;
|
||||
border-radius: 6px;
|
||||
white-space: pre-wrap;
|
||||
font-family: var(--font-mono, monospace);
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.modal {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0,0,0,0.5);
|
||||
z-index: 1000;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.modal.open {
|
||||
display: flex;
|
||||
}
|
||||
.modal-content {
|
||||
background: var(--background-color-default, #ffffff);
|
||||
border: 1px solid var(--border-color-default, #d0d7de);
|
||||
border-radius: 8px;
|
||||
padding: 1.5rem;
|
||||
max-width: 600px;
|
||||
width: 90%;
|
||||
max-height: 80vh;
|
||||
overflow-y: auto;
|
||||
position: relative;
|
||||
}
|
||||
.modal-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.modal-title {
|
||||
font-size: var(--text-title-medium, 20px);
|
||||
font-weight: var(--font-weight-semibold, 600);
|
||||
}
|
||||
.modal-number {
|
||||
color: var(--text-color-muted, #656d76);
|
||||
font-size: 12px;
|
||||
}
|
||||
.close-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 24px;
|
||||
cursor: pointer;
|
||||
color: var(--text-color-default, #1f2328);
|
||||
padding: 0;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.close-btn:hover {
|
||||
background: var(--background-color-secondary, #f6f8fa);
|
||||
}
|
||||
.modal-body {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
.modal-description {
|
||||
color: var(--text-color-default, #1f2328);
|
||||
margin-bottom: 1rem;
|
||||
line-height: var(--leading-body-medium, 20px);
|
||||
}
|
||||
.modal-description h1, .modal-description h2, .modal-description h3 {
|
||||
font-weight: var(--font-weight-semibold, 600);
|
||||
margin: 1rem 0 0.5rem;
|
||||
border-bottom: 1px solid var(--border-color-default, #d1d9e0);
|
||||
padding-bottom: 0.25rem;
|
||||
}
|
||||
.modal-description h1 { font-size: 1.2em; }
|
||||
.modal-description h2 { font-size: 1.1em; }
|
||||
.modal-description h3 { font-size: 1em; }
|
||||
.modal-description p { margin: 0.5rem 0; }
|
||||
.modal-description ul, .modal-description ol {
|
||||
padding-left: 1.5rem;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
.modal-description li { margin: 0.25rem 0; }
|
||||
.modal-description a {
|
||||
color: var(--color-accent-fg, #0969da);
|
||||
text-decoration: none;
|
||||
}
|
||||
.modal-description a:hover { text-decoration: underline; }
|
||||
.modal-description code {
|
||||
font-family: var(--font-mono, monospace);
|
||||
font-size: var(--text-code-inline, 12px);
|
||||
background: var(--background-color-secondary, #f6f8fa);
|
||||
padding: 0.1em 0.3em;
|
||||
border-radius: 3px;
|
||||
}
|
||||
.modal-description pre {
|
||||
background: var(--background-color-secondary, #f6f8fa);
|
||||
padding: 0.75rem;
|
||||
border-radius: 6px;
|
||||
overflow-x: auto;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
.modal-description pre code {
|
||||
background: none;
|
||||
padding: 0;
|
||||
}
|
||||
.modal-description blockquote {
|
||||
border-left: 3px solid var(--border-color-default, #d1d9e0);
|
||||
padding-left: 0.75rem;
|
||||
margin: 0.5rem 0;
|
||||
color: var(--text-color-muted, #656d76);
|
||||
}
|
||||
.modal-description hr {
|
||||
border: none;
|
||||
border-top: 1px solid var(--border-color-default, #d1d9e0);
|
||||
margin: 0.75rem 0;
|
||||
}
|
||||
.modal-description input[type="checkbox"] { margin-right: 0.3em; }
|
||||
.modal-meta {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
font-size: 12px;
|
||||
color: var(--text-color-muted, #656d76);
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.modal-meta-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
.modal-labels {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.label-badge {
|
||||
background: #f0f6ff;
|
||||
color: #0969da;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 3px;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
}
|
||||
.label-badge.requires { background: #fef2f1; color: #cf222e; }
|
||||
.label-badge.ready { background: #f0f6ff; color: #0969da; }
|
||||
.label-badge.approved { background: #f0f6ff; color: #1a7f34; }
|
||||
.label-badge.rejected { background: #f6f8fa; color: var(--text-color-muted, #656d76); }
|
||||
.modal-pr {
|
||||
border-top: 1px solid var(--border-color-default, #d0d7de);
|
||||
padding-top: 1rem;
|
||||
}
|
||||
.pr-link {
|
||||
color: #0969da;
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
}
|
||||
.pr-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>External Plugins Board</h1>
|
||||
<div id="content"><div class="loading">Loading issues...</div></div>
|
||||
|
||||
<div id="modal" class="modal">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<div>
|
||||
<div class="modal-number" id="modalNumber"></div>
|
||||
<div class="modal-title" id="modalTitle"></div>
|
||||
</div>
|
||||
<button class="close-btn" onclick="closeModal()">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="modal-description" id="modalDescription"></div>
|
||||
<div class="modal-meta" id="modalMeta"></div>
|
||||
<div class="modal-labels" id="modalLabels"></div>
|
||||
<div class="modal-pr" id="modalPR" style="display: none;"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const STATES = [
|
||||
{ key: 'requires-submitter-fixes', label: 'Requires Submitter Fixes' },
|
||||
{ key: 'ready-for-review', label: 'Ready for Review' },
|
||||
{ key: 'approved', label: 'Approved' },
|
||||
{ key: 'rejected', label: 'Rejected' }
|
||||
];
|
||||
let draggedIssue = null;
|
||||
let allIssues = [];
|
||||
|
||||
function formatDate(dateStr) {
|
||||
const date = new Date(dateStr);
|
||||
return date.toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' });
|
||||
}
|
||||
|
||||
function getLabelClass(labelName) {
|
||||
if (labelName.includes('requires')) return 'requires';
|
||||
if (labelName.includes('ready')) return 'ready';
|
||||
if (labelName.includes('approved')) return 'approved';
|
||||
if (labelName.includes('rejected')) return 'rejected';
|
||||
return '';
|
||||
}
|
||||
|
||||
function showIssueModal(issueNumber) {
|
||||
const issue = allIssues.find(i => i.number === issueNumber);
|
||||
if (!issue) return;
|
||||
|
||||
document.getElementById('modalNumber').textContent = '#' + issue.number;
|
||||
document.getElementById('modalTitle').textContent = issue.title;
|
||||
document.getElementById('modalDescription').innerHTML = issue.bodyHtml || '';
|
||||
|
||||
const metaHtml = \`
|
||||
<div class="modal-meta-item">
|
||||
<span style="color: var(--text-color-muted);">Created</span>
|
||||
<span>\${formatDate(issue.created_at)}</span>
|
||||
</div>
|
||||
<div class="modal-meta-item">
|
||||
<span style="color: var(--text-color-muted);">Updated</span>
|
||||
<span>\${formatDate(issue.updated_at)}</span>
|
||||
</div>
|
||||
\`;
|
||||
document.getElementById('modalMeta').innerHTML = metaHtml;
|
||||
|
||||
const labelsHtml = (issue.labels || []).map(l =>
|
||||
\`<span class="label-badge \${getLabelClass(l.name)}">\${l.name.replace(/-/g, ' ')}</span>\`
|
||||
).join('');
|
||||
document.getElementById('modalLabels').innerHTML = labelsHtml;
|
||||
|
||||
const prDiv = document.getElementById('modalPR');
|
||||
if (issue.pr_url) {
|
||||
prDiv.innerHTML = \`<a href="\${issue.pr_url}" target="_blank" class="pr-link">View generated PR →</a>\`;
|
||||
prDiv.style.display = 'block';
|
||||
} else {
|
||||
prDiv.style.display = 'none';
|
||||
}
|
||||
|
||||
document.getElementById('modal').classList.add('open');
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
document.getElementById('modal').classList.remove('open');
|
||||
}
|
||||
|
||||
async function loadIssues() {
|
||||
try {
|
||||
const response = await fetch('/api/issues');
|
||||
const data = await response.json();
|
||||
if (!response.ok) throw new Error(data.error || 'Failed to load');
|
||||
allIssues = data;
|
||||
render(data);
|
||||
} catch (err) {
|
||||
document.getElementById('content').innerHTML = '<div class="error">Error: ' + err.message + '</div>';
|
||||
}
|
||||
}
|
||||
|
||||
function render(issues) {
|
||||
const board = document.createElement('div');
|
||||
board.className = 'board';
|
||||
|
||||
STATES.forEach(state => {
|
||||
const column = document.createElement('div');
|
||||
column.className = 'column';
|
||||
column.dataset.state = state.key;
|
||||
|
||||
const header = document.createElement('div');
|
||||
header.className = 'column-header ' + state.key.split('-')[0];
|
||||
header.textContent = state.label;
|
||||
column.appendChild(header);
|
||||
|
||||
const issuesContainer = document.createElement('div');
|
||||
issuesContainer.className = 'issues';
|
||||
|
||||
const stateIssues = issues.filter(issue => {
|
||||
return (issue.labels || []).some(l => l.name === state.key);
|
||||
});
|
||||
|
||||
stateIssues.forEach(issue => {
|
||||
const card = document.createElement('div');
|
||||
card.className = 'issue-card';
|
||||
card.draggable = true;
|
||||
card.dataset.issue = issue.number;
|
||||
|
||||
const num = document.createElement('div');
|
||||
num.className = 'issue-number';
|
||||
num.textContent = '#' + issue.number;
|
||||
card.appendChild(num);
|
||||
|
||||
const title = document.createElement('div');
|
||||
title.className = 'issue-title';
|
||||
title.textContent = issue.title;
|
||||
card.appendChild(title);
|
||||
|
||||
if (issue.pr_url) {
|
||||
const link = document.createElement('a');
|
||||
link.className = 'issue-link';
|
||||
link.href = issue.pr_url;
|
||||
link.target = '_blank';
|
||||
link.textContent = 'View PR →';
|
||||
card.appendChild(link);
|
||||
}
|
||||
|
||||
card.addEventListener('click', (e) => {
|
||||
if (e.target.tagName !== 'A') {
|
||||
showIssueModal(issue.number);
|
||||
}
|
||||
});
|
||||
|
||||
card.addEventListener('dragstart', () => {
|
||||
draggedIssue = issue.number;
|
||||
card.classList.add('dragging');
|
||||
});
|
||||
card.addEventListener('dragend', () => {
|
||||
card.classList.remove('dragging');
|
||||
});
|
||||
|
||||
issuesContainer.appendChild(card);
|
||||
});
|
||||
|
||||
column.appendChild(issuesContainer);
|
||||
board.appendChild(column);
|
||||
});
|
||||
|
||||
document.getElementById('content').innerHTML = '';
|
||||
document.getElementById('content').appendChild(board);
|
||||
}
|
||||
|
||||
document.addEventListener('dragover', e => {
|
||||
e.preventDefault();
|
||||
e.dataTransfer.dropEffect = 'move';
|
||||
});
|
||||
|
||||
document.addEventListener('drop', async e => {
|
||||
e.preventDefault();
|
||||
const column = e.target.closest('.column');
|
||||
if (column && draggedIssue) {
|
||||
try {
|
||||
const response = await fetch('/api/issues/update', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ issueNumber: draggedIssue, newState: column.dataset.state })
|
||||
});
|
||||
if (response.ok) await loadIssues();
|
||||
} catch (err) {
|
||||
console.error('Error:', err);
|
||||
}
|
||||
draggedIssue = null;
|
||||
}
|
||||
});
|
||||
|
||||
// Close modal when clicking outside
|
||||
document.getElementById('modal').addEventListener('click', (e) => {
|
||||
if (e.target.id === 'modal') closeModal();
|
||||
});
|
||||
|
||||
loadIssues();
|
||||
</script>
|
||||
</body>
|
||||
</html>`;
|
||||
}
|
||||
|
||||
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()));
|
||||
}
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
Reference in New Issue
Block a user