Files
store/marketplace.sune

27 lines
13 KiB
JSON

[
{
"id": "e1yibwd",
"name": "Marketplace",
"pinned": false,
"avatar": "",
"url": "gh://sune-org/store@main/marketplace.sune",
"updatedAt": 1756950687029,
"settings": {
"model": "openai/gpt-5",
"temperature": 1,
"top_p": 0.97,
"top_k": 0,
"frequency_penalty": 0,
"presence_penalty": 0,
"repetition_penalty": 1,
"min_p": 0,
"top_a": 0,
"max_tokens": 0,
"verbosity": "",
"reasoning_effort": "default",
"system_prompt": "",
"html": "<!--\n Sune: Marketplace\n Version: 1.8.1\n Description: Discover and install new sunes from the official sune-org/store repository.\n-->\n<div id=\"suneMarketplaceContainer\" class=\"p-4 md:p-6 bg-slate-50\">\n <!-- Header -->\n <div class=\"mb-6 pb-4 border-b border-slate-200\">\n <div class=\"flex justify-between items-center\">\n <h1 class=\"text-2xl font-bold text-slate-800\">Sune Marketplace</h1>\n <span class=\"text-xs font-mono text-slate-400\">v1.8.1</span>\n </div>\n <p class=\"mt-1 text-sm text-slate-600\">Discover and install new capabilities for your Sune.</p>\n </div>\n\n <!-- Content Area: Populated by script -->\n <div id=\"marketplaceContent\">\n <div class=\"flex justify-center items-center py-20\">\n <div class=\"animate-spin rounded-full h-12 w-12 border-b-2 border-slate-500\"></div>\n </div>\n </div>\n\n <!-- Log Viewer -->\n <div class=\"mt-8\">\n <div class=\"flex justify-between items-center mb-2\">\n <h3 class=\"text-sm font-semibold text-slate-600\">Installation Log</h3>\n <button id=\"marketplaceClearLogBtn\" class=\"text-xs text-slate-500 hover:text-slate-800 hover:underline\">Clear Log</button>\n </div>\n <div id=\"marketplaceLogContainer\" class=\"bg-slate-800 text-white font-mono text-xs rounded-lg p-4 h-40 overflow-y-auto\" style=\"scroll-behavior: smooth;\">\n </div>\n </div>\n\n <!-- Contribution Snippet -->\n <div class=\"mt-8 pt-6 border-t border-slate-200\">\n <div class=\"text-center bg-white p-6 rounded-lg border border-slate-200 shadow-sm\">\n <div class=\"flex items-center justify-center gap-2\">\n <i data-lucide=\"git-pull-request-arrow\" class=\"h-5 w-5 text-slate-600\"></i>\n <h3 class=\"text-lg font-semibold text-slate-800\">Contribute your Sune</h3>\n </div>\n <p class=\"mt-2 text-sm text-slate-600 max-w-lg mx-auto\">\n Have a sune you'd like to share? Add it by opening a pull request in the\n <a href=\"https://github.com/sune-org/store\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"font-medium text-sky-600 hover:underline\">sune-org/store</a> repository.\n </p>\n </div>\n </div>\n</div>\n\n<script>\n(() => {\n const rootElement = document.getElementById('suneMarketplaceContainer');\n if (!rootElement) {\n console.error(\"Marketplace Sune: Root element #suneMarketplaceContainer not found.\");\n return;\n }\n\n const marketplaceContent = rootElement.querySelector('#marketplaceContent');\n const logContainer = rootElement.querySelector('#marketplaceLogContainer');\n\n window.SuneMarketplace = window.SuneMarketplace || { catalog: null, isLoading: false };\n\n const logMessage = (message, type = 'info') => {\n if (!logContainer) return;\n const entry = document.createElement('div');\n const sanitizedMessage = message.toString().replace(/</g, '&lt;').replace(/>/g, '&gt;');\n let prefix = '»';\n let colorClass = 'text-slate-300';\n switch (type) {\n case 'success': prefix = '✔'; colorClass = 'text-green-400'; break;\n case 'error': prefix = '✖'; colorClass = 'text-red-400'; break;\n case 'warn': prefix = '⚠'; colorClass = 'text-yellow-400'; break;\n }\n entry.className = `flex items-start gap-2 ${colorClass}`;\n entry.innerHTML = `<span class=\"flex-shrink-0 pt-0.5\">${prefix}</span><span class=\"flex-grow\">${sanitizedMessage}</span>`;\n logContainer.appendChild(entry);\n logContainer.scrollTop = logContainer.scrollHeight;\n };\n\n const clearLog = () => {\n if (!logContainer) return;\n logContainer.innerHTML = '';\n logMessage(\"Log cleared.\");\n };\n\n const convertToJsDelivrUrl = (rawUrl) => {\n if (!rawUrl || typeof rawUrl !== 'string' || !rawUrl.startsWith('https://raw.githubusercontent.com/')) return rawUrl;\n try {\n const url = new URL(rawUrl);\n const [, user, repo, branch, ...filePath] = url.pathname.split('/');\n return `https://cdn.jsdelivr.net/gh/${user}/${repo}@${branch}/${filePath.join('/')}`;\n } catch (e) { return rawUrl; }\n };\n\n const convertToShortUrl = (rawUrl) => {\n if (!rawUrl || !rawUrl.startsWith('https://raw.githubusercontent.com/')) return rawUrl;\n try {\n const path = rawUrl.substring('https://raw.githubusercontent.com/'.length);\n const parts = path.split('/');\n const user = parts.shift();\n const repo = parts.shift();\n const branch = parts.shift();\n return `${user}/${repo}@${branch}/${parts.join('/')}`;\n } catch {\n return rawUrl;\n }\n };\n\n const renderMarketplace = (items) => {\n const installedSunes = window.SUNE?.list || [];\n const html = items.map(item => {\n const rawUrl = item.raw;\n const shortUrl = convertToShortUrl(rawUrl);\n const ghUrl = `gh://${shortUrl}`;\n const isInstalled = installedSunes.some(s => s.url === rawUrl || s.url === shortUrl || s.url === ghUrl);\n\n const buttonText = isInstalled ? 'Installed' : 'Install';\n const buttonIcon = isInstalled ? 'check-circle' : 'download-cloud';\n const buttonClasses = isInstalled \n ? \"bg-slate-100 text-slate-500 cursor-default\" \n : \"bg-slate-600 text-white hover:bg-slate-700 active:bg-slate-800 focus:ring-2 focus:ring-offset-2 focus:ring-slate-500\";\n \n const displayName = (item.name || 'Untitled Sune').replace(/\\.sune$/, '').replace(/[-_]/g, ' ').replace(/\\b\\w/g, l => l.toUpperCase());\n const description = item.description || \"Official utility sune to enhance workflow.\";\n\n return `\n <div class=\"bg-white border border-slate-200 rounded-lg shadow-sm p-5 flex flex-col justify-between transition-shadow hover:shadow-md\">\n <div>\n <h3 class=\"text-base font-semibold text-slate-800\">${displayName}</h3>\n <p class=\"text-sm text-slate-500 mt-1 h-10 overflow-hidden\">${description}</p>\n </div>\n <button \n data-name=\"${displayName}\" \n data-short-url=\"${shortUrl}\" \n data-raw-url=\"${rawUrl}\"\n class=\"install-btn mt-4 w-full px-4 py-2 text-sm font-medium rounded-md transition-colors duration-200 flex items-center justify-center gap-2 ${buttonClasses}\" \n ${isInstalled ? 'disabled' : ''}>\n <i data-lucide=\"${buttonIcon}\" class=\"h-4 w-4\"></i>\n <span>${buttonText}</span>\n </button>\n </div>`;\n }).join('');\n marketplaceContent.innerHTML = `<div class=\"grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4\">${html}</div>`;\n window.lucide?.createIcons();\n };\n\n const handleInstallClick = async (button) => {\n if (button.disabled) return;\n \n const displayName = button.dataset.name || 'Unknown Sune';\n const rawUrl = button.dataset.rawUrl;\n const shortUrl = button.dataset.shortUrl;\n const ghUrl = `gh://${shortUrl}`;\n const fetchUrl = convertToJsDelivrUrl(rawUrl);\n\n clearLog();\n logMessage(`Starting installation for '${displayName}'...`);\n\n button.disabled = true;\n button.innerHTML = `<i data-lucide=\"loader-circle\" class=\"h-4 w-4 animate-spin\"></i><span>Installing...</span>`;\n window.lucide?.createIcons();\n\n try {\n logMessage(`Fetching from: ${fetchUrl}`);\n const response = await fetch(fetchUrl);\n if (!response.ok) throw new Error(`Fetch failed with status: ${response.status}`);\n\n const suneDataArray = await response.json();\n if (!Array.isArray(suneDataArray)) throw new Error('Invalid or empty sune file format.');\n \n logMessage(`Parsed ${suneDataArray.length} sune(s) from file.`);\n logMessage(`Assigning sync URL: ${ghUrl}`);\n\n const allSunes = window.SUNE.list;\n let added = 0, updated = 0, skipped = 0;\n\n for (const suneObject of suneDataArray) {\n if (!suneObject || !suneObject.id) {\n logMessage('Skipping invalid sune entry without an ID.', 'warn');\n skipped++;\n continue;\n }\n \n suneObject.url = ghUrl; // Assign the correct gh:// prefixed URL\n const existing = allSunes.find(s => s.id === suneObject.id);\n\n if (!existing) {\n window.SUNE.create(suneObject);\n added++;\n logMessage(`Installed: '${suneObject.name || suneObject.id}'.`, 'success');\n } else if (+(suneObject.updatedAt || 0) > +(existing.updatedAt || 0)) {\n Object.assign(existing, suneObject);\n updated++;\n logMessage(`Updated: '${suneObject.name || suneObject.id}'.`, 'success');\n } else {\n skipped++;\n }\n }\n\n if (updated > 0) {\n window.SUNE.save(); // Persist changes from Object.assign\n }\n if (skipped > 0 && added === 0 && updated === 0) {\n logMessage('All sunes are already up to date.', 'info');\n }\n if (added > 0 || updated > 0) {\n logMessage('Refreshing sune list...', 'success');\n window.renderSidebar();\n }\n\n button.innerHTML = `<i data-lucide=\"check-circle\" class=\"h-4 w-4\"></i><span>${updated > 0 ? 'Updated' : 'Installed'}</span>`;\n button.classList.remove('bg-slate-600', 'hover:bg-slate-700');\n button.classList.add('bg-slate-100', 'text-slate-500', 'cursor-default');\n\n } catch (error) {\n logMessage(`Installation failed: ${error.message}`, 'error');\n button.disabled = false;\n button.innerHTML = `<i data-lucide=\"alert-triangle\" class=\"h-4 w-4\"></i><span>Retry Install</span>`;\n } finally {\n window.lucide?.createIcons();\n }\n };\n \n const initMarketplace = async () => {\n clearLog();\n if (window.SuneMarketplace.catalog) {\n renderMarketplace(window.SuneMarketplace.catalog);\n logMessage(\"Loaded cached catalog.\");\n return;\n }\n if (window.SuneMarketplace.isLoading) return;\n \n try {\n window.SuneMarketplace.isLoading = true;\n logMessage(\"Fetching sune catalog...\");\n \n const catalogUrl = 'https://cdn.jsdelivr.net/gh/sune-org/store@main/catalog.json';\n const response = await fetch(catalogUrl, { cache: 'no-store' });\n if (!response.ok) throw new Error(`Catalog fetch failed: Status ${response.status}`);\n \n const catalogData = (await response.json()).filter(i => i.name && i.name.endsWith('.sune'));\n window.SuneMarketplace.catalog = catalogData;\n logMessage(`Catalog loaded with ${catalogData.length} items.`);\n renderMarketplace(catalogData);\n } catch (error) {\n logMessage(`Error initializing marketplace: ${error.message}`, 'error');\n marketplaceContent.innerHTML = `<div class=\"text-center py-20 bg-red-50 text-red-700 rounded-lg\"><p>Could not load Marketplace.</p><p class=\"text-sm mt-2\">${error.message}</p></div>`;\n } finally {\n window.SuneMarketplace.isLoading = false;\n }\n };\n \n rootElement.addEventListener('click', (event) => {\n const installButton = event.target.closest('.install-btn');\n if (installButton) {\n handleInstallClick(installButton);\n return;\n }\n if (event.target.closest('#marketplaceClearLogBtn')) {\n clearLog();\n return;\n }\n });\n\n initMarketplace();\n window.lucide?.createIcons();\n})();\n</script>\n",
"extension_html": "<sune src='https://raw.githubusercontent.com/sune-org/store/refs/heads/main/sync.sune' private />"
}
}
]