@@ -25,12 +25,15 @@ import {
2525 parseSkillMetadata ,
2626 parseYamlFile ,
2727} from "./yaml-parser.mjs" ;
28+ import { readExternalPlugins } from "./external-plugin-validation.mjs" ;
2829
2930const __filename = fileURLToPath ( import . meta. url ) ;
3031
3132const WEBSITE_DIR = path . join ( ROOT_FOLDER , "website" ) ;
3233const WEBSITE_DATA_DIR = path . join ( WEBSITE_DIR , "public" , "data" ) ;
3334const WEBSITE_SOURCE_DATA_DIR = path . join ( WEBSITE_DIR , "data" ) ;
35+ const EXTERNAL_CANVAS_KEYWORD = "canvas" ;
36+ const EXTERNAL_CANVAS_PREVIEW_PATH = "assets/preview.png" ;
3437
3538/**
3639 * Ensure the output directory exists
@@ -97,6 +100,23 @@ function normalizeText(value, fallback = "") {
97100 return typeof value === "string" ? value . trim ( ) : fallback ;
98101}
99102
103+ function normalizeRepoRelativePath ( value ) {
104+ const normalized = normalizeText ( value ) ;
105+ if ( ! normalized || normalized === "/" ) {
106+ return "" ;
107+ }
108+
109+ return normalized . replace ( / \\ / g, "/" ) . replace ( / ^ \/ + | \/ + $ / g, "" ) ;
110+ }
111+
112+ function joinRepoPath ( ...segments ) {
113+ return segments
114+ . map ( ( segment ) => String ( segment ?? "" ) . trim ( ) )
115+ . filter ( Boolean )
116+ . join ( "/" )
117+ . replace ( / \/ + / g, "/" ) ;
118+ }
119+
100120/**
101121 * Normalize an author value (npm string form or { name, url } object) to
102122 * { name, url? } | null. Returns null when no usable name is present.
@@ -1055,6 +1075,67 @@ function normalizeExternalScreenshotRole(value, ref) {
10551075 } ;
10561076}
10571077
1078+ function buildExternalRepoImageUrl ( repo , locator , assetPath ) {
1079+ if ( ! repo || ! locator || ! assetPath ) {
1080+ return null ;
1081+ }
1082+
1083+ const encodedLocator = locator
1084+ . split ( "/" )
1085+ . map ( ( segment ) => encodeURIComponent ( segment ) )
1086+ . join ( "/" ) ;
1087+ const encodedPath = assetPath
1088+ . split ( "/" )
1089+ . map ( ( segment ) => encodeURIComponent ( segment ) )
1090+ . join ( "/" ) ;
1091+ return `https://raw.githubusercontent.com/${ repo } /${ encodedLocator } /${ encodedPath } ` ;
1092+ }
1093+
1094+ function buildExternalRepoTreeUrl ( repo , locator , pluginRoot ) {
1095+ if ( ! repo ) {
1096+ return null ;
1097+ }
1098+
1099+ if ( locator ) {
1100+ const treePath = normalizeRepoRelativePath ( pluginRoot ) ;
1101+ const encodedLocator = locator
1102+ . split ( "/" )
1103+ . map ( ( segment ) => encodeURIComponent ( segment ) )
1104+ . join ( "/" ) ;
1105+ const encodedTreePath = treePath
1106+ ? treePath
1107+ . split ( "/" )
1108+ . map ( ( segment ) => encodeURIComponent ( segment ) )
1109+ . join ( "/" )
1110+ : null ;
1111+ const suffix = encodedTreePath ? `/${ encodedTreePath } ` : "" ;
1112+ return `https://github.com/${ repo } /tree/${ encodedLocator } ${ suffix } ` ;
1113+ }
1114+
1115+ return `https://github.com/${ repo } ` ;
1116+ }
1117+
1118+ function hasCanvasKeyword ( plugin ) {
1119+ return normalizeExternalKeywords ( plugin ) . some (
1120+ ( keyword ) => normalizeText ( keyword ) . toLowerCase ( ) === EXTERNAL_CANVAS_KEYWORD
1121+ ) ;
1122+ }
1123+
1124+ function normalizeExternalKeywords ( plugin ) {
1125+ const source = Array . isArray ( plugin ?. keywords )
1126+ ? plugin . keywords
1127+ : Array . isArray ( plugin ?. tags )
1128+ ? plugin . tags
1129+ : [ ] ;
1130+
1131+ return [ ...new Set (
1132+ source
1133+ . filter ( ( keyword ) => typeof keyword === "string" )
1134+ . map ( ( keyword ) => keyword . trim ( ) )
1135+ . filter ( Boolean )
1136+ ) ] . sort ( ( a , b ) => a . localeCompare ( b ) ) ;
1137+ }
1138+
10581139function normalizeExtensionScreenshotRole ( value , relPath , ref ) {
10591140 if ( ! value ) return null ;
10601141 if ( typeof value === "string" ) {
@@ -1320,6 +1401,93 @@ function generateCanvasManifest(gitDates, commitSha) {
13201401 }
13211402 }
13221403
1404+ const seenExtensionIds = new Set ( items . map ( ( item ) => String ( item . id ) . toLowerCase ( ) ) ) ;
1405+ const {
1406+ plugins : externalPlugins ,
1407+ errors : externalPluginErrors ,
1408+ warnings : externalPluginWarnings ,
1409+ } = readExternalPlugins ( { policy : "marketplace" } ) ;
1410+ externalPluginWarnings . forEach ( ( warning ) => console . warn ( `Warning: ${ warning } ` ) ) ;
1411+ if ( externalPluginErrors . length > 0 ) {
1412+ externalPluginErrors . forEach ( ( error ) => console . error ( `Error: ${ error } ` ) ) ;
1413+ throw new Error ( "External plugin validation failed" ) ;
1414+ }
1415+
1416+ for ( const ext of externalPlugins ) {
1417+ if ( ! hasCanvasKeyword ( ext ) ) {
1418+ continue ;
1419+ }
1420+
1421+ const name = normalizeText ( ext ?. name ) ;
1422+ if ( ! name ) {
1423+ continue ;
1424+ }
1425+ const displayName = formatDisplayName ( name ) ;
1426+
1427+ const id = normalizeText ( ext ?. name ) . toLowerCase ( ) . replace ( / \s + / g, "-" ) ;
1428+ if ( seenExtensionIds . has ( id ) ) {
1429+ continue ;
1430+ }
1431+
1432+ const source = ext ?. source ;
1433+ if ( source ?. source !== "github" || ! normalizeText ( source ?. repo ) ) {
1434+ console . warn ( `Warning: skipping external canvas "${ name } " due to missing GitHub source` ) ;
1435+ continue ;
1436+ }
1437+
1438+ const locator = normalizeText ( source . sha ) || normalizeText ( source . ref ) ;
1439+ if ( ! locator ) {
1440+ console . warn ( `Warning: skipping external canvas "${ name } " because source.sha or source.ref is required` ) ;
1441+ continue ;
1442+ }
1443+
1444+ const pluginRoot = normalizeRepoRelativePath ( source . path ) ;
1445+ const previewPath = joinRepoPath ( pluginRoot , EXTERNAL_CANVAS_PREVIEW_PATH ) ;
1446+ const imageUrl = buildExternalRepoImageUrl ( source . repo , locator , previewPath ) ;
1447+ const sourceUrl = buildExternalRepoTreeUrl ( source . repo , locator , pluginRoot ) ;
1448+ const externalSource = normalizeText ( source . repo ) ;
1449+ const keywords = normalizeExternalKeywords ( ext ) ;
1450+
1451+ items . push ( {
1452+ id,
1453+ canvasId : id ,
1454+ extensionId : id ,
1455+ extensionName : name ,
1456+ pluginName : null ,
1457+ name : displayName ,
1458+ version : normalizeText ( ext ?. version , "1.0.0" ) ,
1459+ readmeFile : null ,
1460+ description : normalizeText ( ext ?. description , "External canvas extension" ) ,
1461+ path : null ,
1462+ ref : null ,
1463+ lastUpdated : null ,
1464+ screenshots : {
1465+ icon : imageUrl
1466+ ? {
1467+ path : imageUrl ,
1468+ type : getImageMimeType ( EXTERNAL_CANVAS_PREVIEW_PATH ) ,
1469+ }
1470+ : null ,
1471+ gallery : imageUrl
1472+ ? {
1473+ path : imageUrl ,
1474+ type : getImageMimeType ( EXTERNAL_CANVAS_PREVIEW_PATH ) ,
1475+ }
1476+ : null ,
1477+ } ,
1478+ imageUrl,
1479+ assetPath : null ,
1480+ installUrl : null ,
1481+ installCommand : null ,
1482+ sourceUrl,
1483+ externalSource,
1484+ external : true ,
1485+ author : normalizeAuthor ( ext ?. author ) ,
1486+ keywords,
1487+ } ) ;
1488+ seenExtensionIds . add ( id ) ;
1489+ }
1490+
13231491 const sortedItems = items . sort ( ( a , b ) => a . name . localeCompare ( b . name ) ) ;
13241492 const keywordFilters = [ ...new Set ( sortedItems . flatMap ( ( item ) => item . keywords || [ ] ) ) ]
13251493 . filter ( Boolean )
0 commit comments