mirror of
https://github.com/github/awesome-copilot.git
synced 2026-07-17 03:13:25 +00:00
chore: publish from main
This commit is contained in:
@@ -25,12 +25,15 @@ import {
|
||||
parseSkillMetadata,
|
||||
parseYamlFile,
|
||||
} from "./yaml-parser.mjs";
|
||||
import { readExternalPlugins } from "./external-plugin-validation.mjs";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
|
||||
const WEBSITE_DIR = path.join(ROOT_FOLDER, "website");
|
||||
const WEBSITE_DATA_DIR = path.join(WEBSITE_DIR, "public", "data");
|
||||
const WEBSITE_SOURCE_DATA_DIR = path.join(WEBSITE_DIR, "data");
|
||||
const EXTERNAL_CANVAS_KEYWORD = "canvas";
|
||||
const EXTERNAL_CANVAS_PREVIEW_PATH = "assets/preview.png";
|
||||
|
||||
/**
|
||||
* Ensure the output directory exists
|
||||
@@ -97,6 +100,23 @@ function normalizeText(value, fallback = "") {
|
||||
return typeof value === "string" ? value.trim() : fallback;
|
||||
}
|
||||
|
||||
function normalizeRepoRelativePath(value) {
|
||||
const normalized = normalizeText(value);
|
||||
if (!normalized || normalized === "/") {
|
||||
return "";
|
||||
}
|
||||
|
||||
return normalized.replace(/\\/g, "/").replace(/^\/+|\/+$/g, "");
|
||||
}
|
||||
|
||||
function joinRepoPath(...segments) {
|
||||
return segments
|
||||
.map((segment) => String(segment ?? "").trim())
|
||||
.filter(Boolean)
|
||||
.join("/")
|
||||
.replace(/\/+/g, "/");
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize an author value (npm string form or { name, url } object) to
|
||||
* { name, url? } | null. Returns null when no usable name is present.
|
||||
@@ -1055,6 +1075,67 @@ function normalizeExternalScreenshotRole(value, ref) {
|
||||
};
|
||||
}
|
||||
|
||||
function buildExternalRepoImageUrl(repo, locator, assetPath) {
|
||||
if (!repo || !locator || !assetPath) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const encodedLocator = locator
|
||||
.split("/")
|
||||
.map((segment) => encodeURIComponent(segment))
|
||||
.join("/");
|
||||
const encodedPath = assetPath
|
||||
.split("/")
|
||||
.map((segment) => encodeURIComponent(segment))
|
||||
.join("/");
|
||||
return `https://raw.githubusercontent.com/${repo}/${encodedLocator}/${encodedPath}`;
|
||||
}
|
||||
|
||||
function buildExternalRepoTreeUrl(repo, locator, pluginRoot) {
|
||||
if (!repo) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (locator) {
|
||||
const treePath = normalizeRepoRelativePath(pluginRoot);
|
||||
const encodedLocator = locator
|
||||
.split("/")
|
||||
.map((segment) => encodeURIComponent(segment))
|
||||
.join("/");
|
||||
const encodedTreePath = treePath
|
||||
? treePath
|
||||
.split("/")
|
||||
.map((segment) => encodeURIComponent(segment))
|
||||
.join("/")
|
||||
: null;
|
||||
const suffix = encodedTreePath ? `/${encodedTreePath}` : "";
|
||||
return `https://github.com/${repo}/tree/${encodedLocator}${suffix}`;
|
||||
}
|
||||
|
||||
return `https://github.com/${repo}`;
|
||||
}
|
||||
|
||||
function hasCanvasKeyword(plugin) {
|
||||
return normalizeExternalKeywords(plugin).some(
|
||||
(keyword) => normalizeText(keyword).toLowerCase() === EXTERNAL_CANVAS_KEYWORD
|
||||
);
|
||||
}
|
||||
|
||||
function normalizeExternalKeywords(plugin) {
|
||||
const source = Array.isArray(plugin?.keywords)
|
||||
? plugin.keywords
|
||||
: Array.isArray(plugin?.tags)
|
||||
? plugin.tags
|
||||
: [];
|
||||
|
||||
return [...new Set(
|
||||
source
|
||||
.filter((keyword) => typeof keyword === "string")
|
||||
.map((keyword) => keyword.trim())
|
||||
.filter(Boolean)
|
||||
)].sort((a, b) => a.localeCompare(b));
|
||||
}
|
||||
|
||||
function normalizeExtensionScreenshotRole(value, relPath, ref) {
|
||||
if (!value) return null;
|
||||
if (typeof value === "string") {
|
||||
@@ -1320,6 +1401,93 @@ function generateCanvasManifest(gitDates, commitSha) {
|
||||
}
|
||||
}
|
||||
|
||||
const seenExtensionIds = new Set(items.map((item) => String(item.id).toLowerCase()));
|
||||
const {
|
||||
plugins: externalPlugins,
|
||||
errors: externalPluginErrors,
|
||||
warnings: externalPluginWarnings,
|
||||
} = readExternalPlugins({ policy: "marketplace" });
|
||||
externalPluginWarnings.forEach((warning) => console.warn(`Warning: ${warning}`));
|
||||
if (externalPluginErrors.length > 0) {
|
||||
externalPluginErrors.forEach((error) => console.error(`Error: ${error}`));
|
||||
throw new Error("External plugin validation failed");
|
||||
}
|
||||
|
||||
for (const ext of externalPlugins) {
|
||||
if (!hasCanvasKeyword(ext)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const name = normalizeText(ext?.name);
|
||||
if (!name) {
|
||||
continue;
|
||||
}
|
||||
const displayName = formatDisplayName(name);
|
||||
|
||||
const id = normalizeText(ext?.name).toLowerCase().replace(/\s+/g, "-");
|
||||
if (seenExtensionIds.has(id)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const source = ext?.source;
|
||||
if (source?.source !== "github" || !normalizeText(source?.repo)) {
|
||||
console.warn(`Warning: skipping external canvas "${name}" due to missing GitHub source`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const locator = normalizeText(source.sha) || normalizeText(source.ref);
|
||||
if (!locator) {
|
||||
console.warn(`Warning: skipping external canvas "${name}" because source.sha or source.ref is required`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const pluginRoot = normalizeRepoRelativePath(source.path);
|
||||
const previewPath = joinRepoPath(pluginRoot, EXTERNAL_CANVAS_PREVIEW_PATH);
|
||||
const imageUrl = buildExternalRepoImageUrl(source.repo, locator, previewPath);
|
||||
const sourceUrl = buildExternalRepoTreeUrl(source.repo, locator, pluginRoot);
|
||||
const externalSource = normalizeText(source.repo);
|
||||
const keywords = normalizeExternalKeywords(ext);
|
||||
|
||||
items.push({
|
||||
id,
|
||||
canvasId: id,
|
||||
extensionId: id,
|
||||
extensionName: name,
|
||||
pluginName: null,
|
||||
name: displayName,
|
||||
version: normalizeText(ext?.version, "1.0.0"),
|
||||
readmeFile: null,
|
||||
description: normalizeText(ext?.description, "External canvas extension"),
|
||||
path: null,
|
||||
ref: null,
|
||||
lastUpdated: null,
|
||||
screenshots: {
|
||||
icon: imageUrl
|
||||
? {
|
||||
path: imageUrl,
|
||||
type: getImageMimeType(EXTERNAL_CANVAS_PREVIEW_PATH),
|
||||
}
|
||||
: null,
|
||||
gallery: imageUrl
|
||||
? {
|
||||
path: imageUrl,
|
||||
type: getImageMimeType(EXTERNAL_CANVAS_PREVIEW_PATH),
|
||||
}
|
||||
: null,
|
||||
},
|
||||
imageUrl,
|
||||
assetPath: null,
|
||||
installUrl: null,
|
||||
installCommand: null,
|
||||
sourceUrl,
|
||||
externalSource,
|
||||
external: true,
|
||||
author: normalizeAuthor(ext?.author),
|
||||
keywords,
|
||||
});
|
||||
seenExtensionIds.add(id);
|
||||
}
|
||||
|
||||
const sortedItems = items.sort((a, b) => a.name.localeCompare(b.name));
|
||||
const keywordFilters = [...new Set(sortedItems.flatMap((item) => item.keywords || []))]
|
||||
.filter(Boolean)
|
||||
|
||||
@@ -46,6 +46,7 @@ interface ExtensionEntry extends HeaderItem {
|
||||
installUrl?: string | null;
|
||||
installCommand?: string | null;
|
||||
sourceUrl?: string | null;
|
||||
externalSource?: string | null;
|
||||
external?: boolean;
|
||||
author?: ExtensionAuthor | null;
|
||||
keywords?: string[];
|
||||
@@ -117,6 +118,7 @@ const installUrl = safeUrl(
|
||||
: "")
|
||||
);
|
||||
const sourceUrl = safeUrl(item.sourceUrl);
|
||||
const externalSource = (item.externalSource || "").trim();
|
||||
const installCommand =
|
||||
item.installCommand ||
|
||||
(item.pluginName
|
||||
@@ -124,16 +126,13 @@ const installCommand =
|
||||
: "");
|
||||
|
||||
const githubUrl = isExternal
|
||||
? sourceUrl || installUrl || GITHUB_TREE
|
||||
? sourceUrl || GITHUB_TREE
|
||||
: item.path
|
||||
? `${GITHUB_TREE}/${item.path}`
|
||||
: installUrl || GITHUB_TREE;
|
||||
|
||||
// Sidebar "Source" shows item.path; external extensions have no repo path.
|
||||
const sidebarItem =
|
||||
isExternal && (sourceUrl || installUrl)
|
||||
? { ...item, path: sourceUrl || installUrl }
|
||||
: item;
|
||||
const sidebarItem = isExternal && sourceUrl ? { ...item, path: sourceUrl } : item;
|
||||
|
||||
const shortHash = (value: string) =>
|
||||
/^[0-9a-f]{40}$/i.test(value) ? value.slice(0, 7) : value;
|
||||
@@ -262,22 +261,13 @@ if (!isExternal && item.ref) {
|
||||
>
|
||||
{
|
||||
isExternal ? (
|
||||
<div class="skill-install" slot="install">
|
||||
<p class="skill-install-label">Install this extension</p>
|
||||
<p class="skill-install-note">
|
||||
This extension is maintained in an external repository.
|
||||
</p>
|
||||
{installUrl && (
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-secondary skill-install-url-btn"
|
||||
data-action="copy-install-url"
|
||||
data-install-url={installUrl}
|
||||
>
|
||||
Copy install URL
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<PluginInstall
|
||||
slot="install"
|
||||
isExternal={true}
|
||||
externalSource={externalSource || null}
|
||||
label="Install this extension"
|
||||
note="This extension is maintained in an external repository."
|
||||
/>
|
||||
) : (
|
||||
<PluginInstall
|
||||
slot="install"
|
||||
|
||||
@@ -48,6 +48,7 @@ export interface RenderableExtension {
|
||||
installCommand?: string | null;
|
||||
installUrl?: string | null;
|
||||
sourceUrl?: string | null;
|
||||
externalSource?: string | null;
|
||||
external?: boolean;
|
||||
author?: { name: string; url?: string } | null;
|
||||
}
|
||||
@@ -95,13 +96,20 @@ export function renderExtensionsHtml(items: RenderableExtension[]): string {
|
||||
const sourceUrl = safeUrl(
|
||||
item.sourceUrl || (item.path ? getGitHubUrl(item.path) : "")
|
||||
);
|
||||
const externalSource = (item.externalSource || "").trim();
|
||||
const pluginId = item.pluginName || item.id;
|
||||
const ghappInstallUrl =
|
||||
!item.external && pluginId
|
||||
? `ghapp://plugins/install?source=${encodeURIComponent(
|
||||
`${pluginId}@awesome-copilot`
|
||||
)}`
|
||||
: "";
|
||||
item.external
|
||||
? externalSource
|
||||
? `ghapp://plugins/marketplace/add?source=${encodeURIComponent(
|
||||
externalSource
|
||||
)}`
|
||||
: ""
|
||||
: pluginId
|
||||
? `ghapp://plugins/install?source=${encodeURIComponent(
|
||||
`${pluginId}@awesome-copilot`
|
||||
)}`
|
||||
: "";
|
||||
|
||||
const previewImageUrl = safeUrl(item.imageUrl);
|
||||
const previewMediaHtml = previewImageUrl
|
||||
@@ -152,14 +160,18 @@ export function renderExtensionsHtml(items: RenderableExtension[]): string {
|
||||
</a>`
|
||||
: ""
|
||||
}
|
||||
<button
|
||||
${
|
||||
!item.external
|
||||
? `<button
|
||||
class="btn btn-secondary btn-small copy-install-url-btn"
|
||||
data-install-url="${escapeHtml(installUrl)}"
|
||||
title="Copy fallback URL install target"
|
||||
${installUrl ? "" : "disabled"}
|
||||
>
|
||||
Copy URL
|
||||
</button>
|
||||
</button>`
|
||||
: ""
|
||||
}
|
||||
${
|
||||
sourceUrl
|
||||
? `<a href="${escapeHtml(
|
||||
|
||||
Reference in New Issue
Block a user